Lean 语言参考手册

7.6. Recursive Definitions🔗

允许任意递归函数定义会使 Lean 的逻辑不一致。一般递归使得可以写出环形证明:“命题 P 为真,因为命题 P 为真”。在证明之外,一个无限循环可以被赋予类型 Empty,再结合 Lean.Parser.Term.nomatch : termEmpty match/ex falso. `nomatch e` is of arbitrary type `α : Sort u` if Lean can show that an empty set of patterns is exhaustive given `e`'s type, e.g. because it has no constructors. nomatchEmpty.rec,即可“证明”任意定理。

直接禁止递归函数定义将大幅降低 Lean 的实用性:归纳类型 是定义谓词与数据的关键,而它们本身具有递归结构。此外,多数有用的递归函数并不威胁自洽性,而无限循环通常意味着定义有误而非有意为之。Lean 并未一禁了之,而是要求每个递归函数都以安全的方式定义。在繁释递归定义的过程中,Lean 的繁释器还会同时给出该定义安全的理由。可参阅繁释概览中的 繁释器的输出 一节,了解递归定义繁释在整体繁释流程中的位置。

可以定义的递归函数主要有五类:

结构化递归函数

结构化递归函数接收某个实参,并且仅在该实参的真子项上进行递归调用。严格来说,类型为 索引族 的实参会与其索引成组,把整个集合视作一个整体。 繁释器会把递归翻译成对该实参的 递归器 的调用。 由于每个类型正确的递归器使用都保证避免无限回归,这样的翻译即构成函数终止性的证据。 通过递归器定义的函数应用在定义上等同于递归结果,并且在内核中通常较为高效。

良构关系上的递归

有些函数也难以改写为结构化递归;例如,某个函数之所以终止,是因为随着数组索引增大,索引与数组长度之差在减小,但此时由于增长的是函数的实参本身,Nat.rec 并不适用。 在这种情形下,存在一个随每次递归调用而减少的终止度量,但该度量本身并非函数的一个实参。 这时可以使用 良构递归 来定义函数。 良构递归是一种技术:系统地把“伴随度量递减的递归函数”转化为“基于证明的递归函数”,该证明表明任意度量递减序列最终会在最小值处终止。 用良构递归定义的函数应用不一定与其返回值在定义上相等,但这种相等可以作为命题来证明。 即便存在定义相等,这类函数在计算上仍常常较慢,因为它们需要归约通常很大的证明项。

作为偏不动点的递归函数

一个函数的定义可以理解为一条给出其行为的方程。 在某些情况下,即使该递归函数对所有输入未必终止,仍可证明存在一个满足此规格的函数。 该策略甚至适用于某些函数定义对所有输入未必终止的情形。 由此得到的偏函数作为这些方程的不动点而出现,被称为 偏不动点

尤其是,返回类型位于某些单子中的函数(例如 Option)可以用该策略来定义。 对这类单子函数,Lean 还会生成额外的偏正确性定理。 与良构递归类似,按偏不动点定义的函数应用在定义上不等同于其返回值,但 Lean 会生成定理,在命题层面将该函数与其展开式以及定义中所给的归约行为相等同。

余域非空的偏函数

在许多应用中,某些函数的具体实现并不需要被推理。 一个递归函数可能仅作为证明自动化步骤实现的一部分,或仅是不会被形式化证明正确性的普通程序。 在这类场景中,Lean 内核不需要该定义在“定义相等”或“命题相等”层面成立;只要保持逻辑自洽即可。 被标记为 Lean.Parser.Command.declaration : commandpartial 的函数会被内核视作不透明常量,既不会被展开也不会被归约。 为保持自洽性,唯一的要求是其返回类型可被占据(inhabited)。 偏函数在编译后的代码中仍可照常使用,也可出现在命题与证明中;只是它们在 Lean 逻辑中的等式理论非常薄弱。

不安全的递归定义

不安全定义不受偏定义的任何限制。 它们可自由使用一般递归,并可使用会打破等式理论假设的 Lean 特性,例如强制转换原语(unsafeCast)、检查指针相等(ptrAddrUnsafe),以及观察引用计数isExclusiveUnsafe)。 但凡引用不安全定义的声明本身也必须标记为 Lean.Parser.Command.declaration : commandunsafe,以清楚表明此处不保证逻辑自洽。 在编译后的代码中,不安全操作可用于以更高效的实现替换其他函数的实现,而内核仍然使用原始定义。 被替换的函数可以是不透明的,此时该函数名在逻辑中的等式理论是平凡的;也可以是普通函数,此时逻辑中仍会使用该函数。 请谨慎使用这一特性:逻辑自洽性虽不受威胁,但若不安全实现有误,Lean 程序的实际行为可能会偏离其经验证的逻辑模型。

繁释器输出概览 所述,递归函数的繁释分两阶段进行:

  1. 首先,按“Lean 的核心类型论自带递归定义”的假想来繁释该定义。除了使用递归之外,这个临时定义会被完全繁释。编译器会基于这些临时定义生成代码。

  2. 随后进行终止性分析,尝试用四种技术之一向 Lean 内核证明该函数是可接受的。若定义被标记为 Lean.Parser.Command.declaration : commandunsafeLean.Parser.Command.declaration : commandpartial,则直接采用相应技术。若给出了显式的 Lean.Parser.Command.declaration : commandtermination_by 子句,则只尝试其中指明的技术。若无此类子句,繁释器会进行搜索:依次尝试将每个形参作为结构化递归的候选,并尝试寻找某个随每次递归调用而减少、且具良构关系的度量。

本节描述支配递归函数的规则。介绍互递归之后,将逐一给出这五类递归定义的规范,并讨论各自的推理能力与灵活性之间的权衡。

7.6.1. Mutual Recursion🔗

就像递归定义是在其定义体中提到正在被定义的名字一样,互递归 的定义指的是:它们本身可以是递归的,或彼此相互引用。 要在多个声明之间使用互递归,必须把它们放入一个 互递归块 中。

syntax互递声明块

互递的一般语法为:

command ::= ...
    | mutual
        declaration*
      end

其中各声明必须是定义或定理。

在一个互递声明块中,各声明的名称不在彼此的类型签名的作用域内,但在彼此的定义体中可见。 尽管这些名称不在签名的作用域内,它们也不会被当作自动绑定的隐式参数插入。

互递声明块的作用域

在互递声明块中定义的名称不在彼此的签名作用域内。

mutual abbrev NaturalNum : Type := Nat def n : unknown identifier 'NaturalNum'NaturalNum := 5 end
unknown identifier 'NaturalNum'

若不使用互递块,该定义即可通过:

abbrev NaturalNum : Type := Nat def n : NaturalNum := 5
互递块的作用域与自动隐式参数

在互递声明块中定义的名称不在彼此的签名作用域内。不过,它们也不能作为自动绑定的隐式参数使用:

mutual abbrev α : Type := Nat def identity (x : unknown identifier 'α'α) : unknown identifier 'α'α := x end
unknown identifier 'α'

若改用不同的名称,则会自动添加该隐式参数:

mutual abbrev α : Type := Nat def identity (x : β) : β := x end

递归定义的繁释总是在互递块这一粒度上进行;即便某个声明并不处在互递块中,也会好比其周围包了一层单元素的互递块。 通过 Lean.Parser.Term.letrec : termlet recLean.Parser.Command.declaration : commandwhere 引入的局部定义会被从其上下文提升出去;必要时为捕获到的自由变量引入参数;并被视作 Lean.Parser.Command.mutual : commandmutual 块中的独立定义。 因此,写在 Lean.Parser.Command.declaration : commandwhere 块中的辅助定义,既可以彼此互递归,也可以和所在的主体定义互递归,但它们不能在彼此的类型签名中相互引用。

在繁释的第一步结束后(此时定义仍是递归的),在使用上述技术消解递归之前,Lean 会在互递块中的这些定义里识别出真正(互相)递归的团簇,并按照依赖顺序分别处理它们。

7.6.2. Structural Recursion🔗

Structurally recursive functions are those in which each recursive call is on a structurally smaller term than the argument. The same parameter must decrease in all recursive calls; this parameter is called the decreasing parameter. Structural recursion is stronger than the primitive recursion that recursors provide, because the recursive call can use more deeply nested sub-terms of the argument, rather than only an immediate sub-term. The constructions used to implement structural recursion are, however, implemented using the recursor; these helper constructions are described in the section on inductive types.

The rules that govern structural recursion are fundamentally syntactic in nature. There are many recursive definitions that exhibit structurally recursive computational behavior, but which are not accepted by these rules; this is a fundamental consequence of the analysis being fully automatic. Well-founded recursion provides a semantic approach to demonstrating termination that can be used in situations where a recursive function is not structurally recursive, but it can also be used when a function that computes according to structural recursion doesn't satisfy the syntactic requirements.

Structural Recursion vs Subtraction

The function countdown is structurally recursive. The parameter n was matched against the pattern n' + 1, which means that n' is a direct subterm of n in the second branch of the pattern match:

def countdown (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown n'

Replacing pattern matching with an equivalent Boolean test and subtraction results in an error:

def fail to show termination for countdown' with errors failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' n' failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal n : Nat h✝ : ¬(n == 0) = true n' : Nat := n - 1 ⊢ n - 1 < ncountdown' (n : Nat) : List Nat := if n == 0 then [] else let n' := n - 1 n' :: countdown' n'
fail to show termination for
  countdown'
with errors
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    countdown' n'


failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
n : Nat
h✝ : ¬(n == 0) = true
n' : Nat := n - 1
⊢ n - 1 < n

This is because there was no pattern matching on the parameter n. While this function indeed terminates, the argument that it does so is based on properties of if, the equality test, and subtraction, rather than being a generic feature of Nat being an inductive type. These arguments are expressed using well-founded recursion, and a slight change to the function definition allows Lean's automatic support for well-founded recursion to construct an alternative termination proof. This version branches on the decidability of propositional equality for Nat rather than the result of a Boolean equality test:

def countdown' (n : Nat) : List Nat := if n = 0 then [] else let n' := n - 1 n' :: countdown' n'

Here, Lean's automation automatically constructs a termination proof from facts about propositional equality and subtraction. It uses well-founded recursion rather than structural recursion behind the scenes.

Structural recursion may be used explicitly or automatically. With explicit structural recursion, the function definition declares which parameter is the decreasing parameter. If no termination strategy is explicitly declared, Lean performs a search for a decreasing parameter as well as a decreasing measure for use with well-founded recursion. Explicitly annotating structural recursion has the following benefits:

  • It can speed up elaboration, because no search occurs.

  • It documents the termination argument for readers.

  • In situations where structural recursion is explicitly desired, it prevents the accidental use of well-founded recursion.

7.6.2.1. Explicit Structural Recursion

To explicitly use structural recursion, a function or theorem definition can be annotated with a Lean.Parser.Command.declaration : commandtermination_by structural clause that specifies the decreasing parameter. The decreasing parameter may be a reference to a parameter named in the signature. When the signature specifies a function type, the decreasing parameter may additionally be a parameter not named in the signature; in this case, names for the remaining parameters may be introduced by writing them before an arrow (Lean.Parser.Command.declaration : command=>).

Specifying Decreasing Parameters

When the decreasing parameter is a named parameter to the function, it can be specified by referring to its name.

def half (n : Nat) : Nat := match n with | 0 | 1 => 0 | n + 2 => half n + 1 termination_by structural n

When the decreasing parameter is not named in the signature, a name can be introduced locally in the Lean.Parser.Command.declaration : commandtermination_by clause.

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1 termination_by structural n => n
syntaxExplicit Structural Recursion

The termination_by structural clause introduces a decreasing parameter.

Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferrred termination measure will be suggested.

terminationBy ::= ...
    | Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferrred termination measure will be suggested.

termination_by structural (ident* =>)? term

The identifiers before the optional => can bring function parameters into scope that are not already bound in the declaration header, and the mandatory term must indicate one of the function's parameters, whether introduced in the header or locally in the clause.

The decreasing parameter must satisfy the following conditions:

  • Its type must be an inductive type.

  • If its type is an indexed family, then all indices must be parameters of the function.

  • If the inductive or indexed family of the decreasing parameter has data type parameters, then these data type parameters may themselves only depend on function parameters that are part of the fixed prefix.

A fixed parameter is a function parameter that is passed unmodified in all recursive calls and is not an index of the recursive parameter's type. The fixed prefix is the longest prefix of the function's parameters in which all are fixed.

Ineligible decreasing parameters

The decreasing parameter's type must be an inductive type. In notInductive, a function is specified as the decreasing parameter:

def notInductive (x : Nat Nat) : Nat := notInductive (fun n => x (n+1)) cannot use specified measure for structural recursion: its type is not an inductivetermination_by structural x
cannot use specified measure for structural recursion:
  its type is not an inductive

If the decreasing parameter is an indexed family, all the indices must be variables. In constantIndex, the indexed family Fin' is instead applied to a constant value:

inductive Fin' : Nat Type where | zero : Fin' (n+1) | succ : Fin' n Fin' (n+1) def constantIndex (x : Fin' 100) : Nat := constantIndex .zero cannot use specified measure for structural recursion: its type Fin' is an inductive family and indices are not variables Fin' 100termination_by structural x
cannot use specified measure for structural recursion:
  its type Fin' is an inductive family and indices are not variables
    Fin' 100

The parameters of the decreasing parameter's type must not depend on function parameters that come after varying parameters or indices. In afterVarying, the fixed prefix is empty, because the first parameter n varies, so p is not part of the fixed prefix:

inductive WithParam' (p : Nat) : Nat Type where | zero : WithParam' p (n+1) | succ : WithParam' p n WithParam' p (n+1) failed to infer structural recursion: Cannot use parameter x: failed to eliminate recursive application afterVarying (n + 1) p WithParam'.zero def afterVarying (n : Nat) (p : Nat) (x : WithParam' p n) : Nat := afterVarying (n+1) p .zero termination_by structural x
failed to infer structural recursion:
Cannot use parameter x:
  failed to eliminate recursive application
    afterVarying (n + 1) p WithParam'.zero

Furthermore, every recursive call of the functions must be on a strict sub-term of the decreasing parameter.

  • The decreasing parameter itself is a sub-term, but not a strict sub-term.

  • If a sub-term is the discriminant of a Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expression or other pattern-matching syntax, the pattern that matches the discriminant is a sub-term in the right-hand side of each match alternative. In particular, the rules of match generalization are used to connect the discriminant to the occurrences of the pattern term in the right-hand side; thus, it respects definitional equality. The pattern is a strict sub-term if and only if the discriminant is a strict sub-term.

  • If a sub-term is a constructor applied to arguments, then its recursive arguments are strict sub-terms.

Nested Patterns and Sub-Terms

In the following example, the decreasing parameter n is matched against the nested pattern .succ (.succ n). Therefore .succ (.succ n) is a (non-strict) sub-term of n, and consequently both n and .succ n are strict sub-terms, and the definition is accepted.

def fib : Nat Nat | 0 | 1 => 1 | .succ (.succ n) => fib n + fib (.succ n) termination_by structural n => n

For clarity, this example uses .succ n and .succ (.succ n) instead of the equivalent Nat-specific n+1 and n+2.

Matching on Complex Expressions Can Prevent Elaboration

In the following example, the decreasing parameter n is not directly the discriminant of the Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expression. Therefore, n' is not considered a sub-term of n.

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application half n' def half (n : Nat) : Nat := match Option.some n with | .some (n' + 2) => half n' + 1 | _ => 0 termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    half n'

Using well-founded recursion, and explicitly connecting the discriminant to the pattern of the match, this definition can be accepted.

def half (n : Nat) : Nat := match h : Option.some n with | .some (n' + 2) => half n' + 1 | _ => 0 termination_by n decreasing_by n:Natn':Nath:n = n' + 1 + 1n' < n' + 1 + 1; All goals completed! 🐙

Similarly, the following example fails: although xs.tail would reduce to a strict sub-term of xs, this is not visible to Lean according to the rules above. In particular, xs.tail is not definitionally equal to a strict sub-term of xs.

failed to infer structural recursion: Cannot use parameter #2: failed to eliminate recursive application listLen xs.tail def listLen : List α Nat | [] => 0 | xs => listLen xs.tail + 1 termination_by structural xs => xs
Simultaneous Matching vs Matching Pairs for Structural Recursion

An important consequence of the strategies that are used to prove termination is that simultaneous matching of two discriminants is not equivalent to matching a pair. Simultaneous matching maintains the connection between the discriminants and the patterns, allowing the pattern matching to refine the types of the assumptions in the local context as well as the expected type of the Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match. Essentially, the elaboration rules for Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match treat the discriminants specially, and changing discriminants in a way that preserves the run-time meaning of a program does not necessarily preserve the compile-time meaning.

This function that finds the minimum of two natural numbers is defined by structural recursion on its first parameter:

def min' (n k : Nat) : Nat := match n, k with | 0, _ => 0 | _, 0 => 0 | n' + 1, k' + 1 => min' n' k' + 1 termination_by structural n

Replacing the simultaneous pattern match on both parameters with a match on a pair causes termination analysis to fail:

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application min' n' k' def min' (n k : Nat) : Nat := match (n, k) with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' n' k' + 1 termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    min' n' k'

This is because the analysis only considers direct pattern matching on parameters when matching recursive calls to strictly-smaller argument values. Wrapping the discriminants in a pair breaks the connection.

Structural Recursion Under Pairs

This function that finds the minimum of the two components of a pair can't be elaborated via structural recursion.

failed to infer structural recursion: Cannot use parameter nk: the type Nat × Nat does not have a `.brecOn` recursor def min' (nk : Nat × Nat) : Nat := match nk with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' (n', k') + 1 termination_by structural nk
failed to infer structural recursion:
Cannot use parameter nk:
  the type Nat × Nat does not have a `.brecOn` recursor

This is because the parameter's type, Prod, is not recursive. Thus, its constructor has no recursive parameters that can be exposed by pattern matching.

This definition is accepted using well-founded recursion, however:

def min' (nk : Nat × Nat) : Nat := match nk with | (0, _) => 0 | (_, 0) => 0 | (n' + 1, k' + 1) => min' (n', k') + 1 termination_by nk
Structural Recursion and Definitional Equality

Even though the recursive occurrence of countdown is applied to a term that is not a strict sub-term of the decreasing parameter, the following definition is accepted:

def countdown (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown (n' + 0) termination_by structural n

This is because n' + 0 is definitionally equal to n', which is a strict sub-term of n. Sub-terms that result from pattern matching are connected to the discriminant using the rules for match generalization, which respect definitional equality.

In countdown', the recursive occurrence is applied to 0 + n', which is not definitionally equal to n' because addition on natural numbers is structurally recursive in its second parameter:

failed to infer structural recursion: Cannot use parameter n: failed to eliminate recursive application countdown' (0 + n') def countdown' (n : Nat) : List Nat := match n with | 0 => [] | n' + 1 => n' :: countdown' (0 + n') termination_by structural n
failed to infer structural recursion:
Cannot use parameter n:
  failed to eliminate recursive application
    countdown' (0 + n')

7.6.2.2. Mutual Structural Recursion🔗

Lean supports the definition of mutually recursive functions using structural recursion. Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : termlet rec expressions and Lean.Parser.Command.declaration : commandwhere blocks. The rules for mutual structural recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups. If every function in the mutual group has a termination_by structural annotation indicating that function’s decreasing argument, then structural recursion is used to translate the definitions.

The requirements on the decreasing argument above are extended:

  • All the types of all the decreasing arguments must be from the same inductive type, or more generally from the same mutual group of inductive types.

  • The parameters of the decreasing parameter's types must be the same for all functions, and may depend only on the common fixed prefix of function arguments.

The functions do not have to be in a one-to-one correspondence to the mutual inductive types. Multiple functions can have a decreasing argument of the same type, and not all types that are mutually recursive with the decreasing argument need have a corresponding function.

Mutual Structural Recursion Over Non-Mutual Types

The following example demonstrates mutual recursion over a non-mutual inductive data type:

mutual def even : Nat Prop | 0 => True | n+1 => odd n termination_by structural n => n def odd : Nat Prop | 0 => False | n+1 => even n termination_by structural n => n end
Mutual Structural Recursion Over Mutual Types

The following example demonstrates recursion over mutually inductive types. The functions Exp.size and App.size are mutually recursive.

mutual inductive Exp where | var : String Exp | app : App Exp inductive App where | fn : String App | app : App Exp App end mutual def Exp.size : Exp Nat | .var _ => 1 | .app a => a.size termination_by structural e => e def App.size : App Nat | .fn _ => 1 | .app a e => a.size + e.size + 1 termination_by structural a => a end

The definition of App.numArgs is structurally recursive over type App. It demonstrates that not all inductive types in the mutual group need to be handled.

def App.numArgs : App Nat | .fn _ => 0 | .app a _ => a.numArgs + 1 termination_by structural a => a

7.6.2.3. Inferring Structural Recursion🔗

If no termination_by clauses are present in a recursive or mutually recursive function definition, then Lean attempts to infer a suitable structurally decreasing argument, effectively by trying all suitable parameters in sequence. If this search fails, Lean then attempts to infer well-founded recursion.

For mutually recursive functions, all combinations of parameters are tried, up to a limit to avoid combinatorial explosion. If only some of the mutually recursive functions have termination_by structural clauses, then only those parameters are considered, while for the other functions all parameters are considered for structural recursion.

A termination_by? clause causes the inferred termination annotation to be shown. It can be automatically added to the source file using the offered suggestion or code action.

Inferred Termination Annotations

Lean automatically infers that the function half is structurally recursive. The termination_by? clause causes the inferred termination annotation to be displayed, and it can be automatically added to the source file with a single click.

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1 Try this: termination_by structural x => xtermination_by?
Try this: termination_by structural x => x

7.6.2.4. Elaboration Using Course-of-Values Recursion🔗

In this section, the construction used to elaborate structurally recursive functions is explained in more detail. This elaboration uses the below and brecOn constructions that are automatically generated from inductive types' recursors.

Recursion vs Recursors

Addition of natural numbers can be defined via recursion on the second argument. This function is straightforwardly structurally recursive.

def add (n : Nat) : Nat Nat | .zero => n | .succ k => .succ (add n k)

Defined using Nat.rec, it is much further from the notations that most people are used to.

def add' (n : Nat) := Nat.rec (motive := fun _ => Nat) n (fun k soFar => .succ soFar)

Structural recursive calls made on data that isn't the immediate child of the function parameter requires either creativity or a complex yet systematic encoding.

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1

One way to think about this function is as a structural recursion that flips a bit at each call, only incrementing the result when the bit is set.

def helper : Nat Bool Nat := Nat.rec (motive := fun _ => Bool Nat) (fun _ => 0) (fun _ soFar => fun b => (if b then Nat.succ else id) (soFar !b)) def half' (n : Nat) : Nat := helper n false [0, 0, 1, 1, 2, 2, 3, 3, 4]#eval [0, 1, 2, 3, 4, 5, 6, 7, 8].map half'
[0, 0, 1, 1, 2, 2, 3, 3, 4]

Instead of creativity, a general technique called course-of-values recursion can be used. Course-of-values recursion uses helpers that can be systematically derived for every inductive type, defined in terms of the recursor; Lean derives them automatically. For every Nat n, the type n.below (motive := mot) provides a value of type mot k for all k < n, represented as an iterated dependent pair type. The course-of-values recursor Nat.brecOn allows a function to use the result for any smaller Nat. Using it to define the function is inconvenient:

noncomputable def half'' (n : Nat) : Nat := Nat.brecOn n (motive := fun _ => Nat) fun k soFar => match k, soFar with | 0, _ | 1, _ => 0 | _ + 2, _, h, _ => h + 1

The function is marked Lean.Parser.Command.declaration : commandnoncomputable because the compiler doesn't support generating code for course-of-values recursion, which is intended for reasoning rather that efficient code. The kernel can still be used to test the function, however:

[0, 0, 1, 1, 2, 2, 3, 3, 4]#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]

The dependent pattern matching in the body of half'' can also be encoded using recursors (specifically, Nat.casesOn), if necessary:

noncomputable def half''' (n : Nat) : Nat := n.brecOn (motive := fun _ => Nat) fun k => k.casesOn (motive := fun k' => (k'.below (motive := fun _ => Nat)) Nat) (fun _ => 0) (fun k' => k'.casesOn (motive := fun k'' => (k''.succ.below (motive := fun _ => Nat)) Nat) (fun _ => 0) (fun _ soFar => soFar.2.1.succ))

This definition still works.

[0, 0, 1, 1, 2, 2, 3, 3, 4]#reduce [0,1,2,3,4,5,6,7,8].map half''
[0, 0, 1, 1, 2, 2, 3, 3, 4]

However, it is now far from the original definition and it has become difficult for most people to understand. Recursors are an excellent logical foundation, but not an easy way to write programs or proofs.

The structural recursion analysis attempts to translate the recursive pre-definition into a use of the appropriate structural recursion constructions. At this step, pattern matching has already been translated into the use of matcher functions; these are treated specially by the termination checker. Next, for each group of parameters, a translation using brecOn is attempted.

Course-of-Values Tables

This definition is equivalent to List.below:

def List.below' {α : Type u} {motive : List α Sort u} : List α Sort (max 1 u) | [] => PUnit | _ :: xs => motive xs ×' xs.below' (motive := motive)

In other words, for a given motive, List.below' is a type that contains a realization of the motive for all suffixes of the list.

More recursive arguments require further nested iterations of the product type. For instance, binary trees have two recursive occurrences.

inductive Tree (α : Type u) : Type u where | leaf | branch (left : Tree α) (val : α) (right : Tree α)

Its corresponding course-of-values table contains the realizations of the motive for all subtrees:

def Tree.below' {α : Type u} {motive : Tree α Sort u} : Tree α Sort (max 1 u) | .leaf => PUnit | .branch left _val right => (motive left ×' left.below' (motive := motive)) ×' (motive right ×' right.below' (motive := motive))

For both lists and trees, the brecOn operator expects just a single case, rather than one per constructor. This case accepts a list or tree along with a table of results for all smaller values; from this, it should satisfy the motive for the provided value. Dependent case analysis of the provided value automatically refines the type of the memo table, providing everything needed.

The following definitions are equivalent to List.brecOn and Tree.brecOn, respectively. The primitive recursive helpers List.brecOnTable and Tree.brecOnTable compute the course-of-values tables along with the final results, and the actual definitions of the brecOn operators simply project out the result.

def List.brecOnTable {α : Type u} {motive : List α Sort u} (xs : List α) (step : (ys : List α) ys.below' (motive := motive) motive ys) : motive xs ×' xs.below' (motive := motive) := match xs with | [] => step [] PUnit.unit, PUnit.unit | x :: xs => let res := xs.brecOnTable (motive := motive) step let val := step (x :: xs) res val, res def Tree.brecOnTable {α : Type u} {motive : Tree α Sort u} (t : Tree α) (step : (ys : Tree α) ys.below' (motive := motive) motive ys) : motive t ×' t.below' (motive := motive) := match t with | .leaf => step .leaf PUnit.unit, PUnit.unit | .branch left val right => let resLeft := left.brecOnTable (motive := motive) step let resRight := right.brecOnTable (motive := motive) step let branchRes := resLeft, resRight let val := step (.branch left val right) branchRes val, branchRes def List.brecOn' {α : Type u} {motive : List α Sort u} (xs : List α) (step : (ys : List α) ys.below' (motive := motive) motive ys) : motive xs := (xs.brecOnTable (motive := motive) step).1 def Tree.brecOn' {α : Type u} {motive : Tree α Sort u} (t : Tree α) (step : (ys : Tree α) ys.below' (motive := motive) motive ys) : motive t := (t.brecOnTable (motive := motive) step).1

The below construction is a mapping from each value of a type to the results of some function call on all smaller values; it can be understood as a memoization table that already contains the results for all smaller values. The notion of “smaller value” that is expressed in the below construction corresponds directly to the definition of strict sub-terms.

Recursors expect an argument for each of the inductive type's constructors; these arguments are called with the constructor's arguments (and the result of recursion on recursive parameters) during ι-reduction. The course-of-values recursion operator brecOn, on the other hand, expects just a single case that covers all constructors at once. This case is provided with a value and a below table that contains the results of recursion on all values smaller than the given value; it should use the contents of the table to satisfy the motive for the provided value. If the function is structurally recursive over a given parameter (or parameter group), then the results of all recursive calls will be present in this table already.

When the body of the recursive function is transformed into an invocation of brecOn on one of the function's parameters, the parameter and its course-of-values table are in scope. The analysis traverses the body of the function, looking for recursive calls. If the parameter is matched against, then its occurrences in the local context are generalized and then instantiated with the pattern; this is also true for the type of the course-of-values table. Typically, this pattern matching results in the type of the course-of-values table becoming more specific, which gives access to the recursive results for smaller values. This generalization process implements the rule that patterns are sub-terms of match discriminants. When an recursive occurrence of the function is detected, the course-of-values table is consulted to see whether it contains a result for the argument being checked. If so, the recursive call can be replaced with a projection from the table. If not, then the parameter in question doesn't support structural recursion.

Elaboration Walkthrough

The first step in walking through the elaboration of half is to manually desugar it to a simpler form. This doesn't match the way Lean works, but its output is much easier to read when there are fewer OfNat instances present. This readable definition:

def half : Nat Nat | 0 | 1 => 0 | n + 2 => half n + 1

can be rewritten to this somewhat lower-level version:

def half : Nat Nat | .zero | .succ .zero => .zero | .succ (.succ n) => half n |>.succ

The elaborator begins by elaborating a pre-definition in which recursion is still present but the definition is otherwise in Lean's core type theory. Turning on the compiler's tracing of pre-definitions, as well as making the pretty printer more explicit, makes the resulting pre-definition visible:

set_option trace.Elab.definition.body true in set_option pp.all true in [Elab.definition.body] half : Nat → Nat := fun (x : Nat) => half.match_1.{1} (fun (x : Nat) => Nat) x (fun (_ : Unit) => Nat.zero) (fun (_ : Unit) => Nat.zero) fun (n : Nat) => Nat.succ (half n)def half : Nat Nat | .zero | .succ .zero => .zero | .succ (.succ n) => half n |>.succ

The returned trace message is:

[Elab.definition.body] half : Nat → Nat :=
    fun (x : Nat) =>
      half.match_1.{1} (fun (x : Nat) => Nat) x
        (fun (_ : Unit) => Nat.zero)
        (fun (_ : Unit) => Nat.zero)
        fun (n : Nat) => Nat.succ (half n)

The auxiliary match function's definition is:

def half.match_1.{u_1} : (motive : Nat → Sort u_1) → (x : Nat) → (Unit → motive Nat.zero) → (Unit → motive 1) → ((n : Nat) → motive n.succ.succ) → motive x := fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n#print half.match_1
def half.match_1.{u_1} : (motive : Nat → Sort u_1) →
  (x : Nat) → (Unit → motive Nat.zero) → (Unit → motive 1) → ((n : Nat) → motive n.succ.succ) → motive x :=
fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n

Formatted more readably, this definition is:

def half.match_1'.{u} : (motive : Nat Sort u) (x : Nat) (Unit motive Nat.zero) (Unit motive 1) ((n : Nat) motive n.succ.succ) motive x := fun motive x h_1 h_2 h_3 => Nat.casesOn x (h_1 ()) fun n => Nat.casesOn n (h_2 ()) fun n => h_3 n

In other words, the specific configuration of patterns used in half are captured in half.match_1.

This definition is a more readable version of half's pre-definition:

def half' : Nat Nat := fun (x : Nat) => half.match_1 (motive := fun _ => Nat) x (fun _ => 0) -- Case for 0 (fun _ => 0) -- Case for 1 (fun n => Nat.succ (half' n)) -- Case for n + 2

To elaborate it as a structurally recursive function, the first step is to establish the bRecOn invocation. The definition must be marked Lean.Parser.Command.declaration : commandnoncomputable because Lean does not support code generation for recursors such as Nat.brecOn.

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => don't know how to synthesize placeholder context: x n : Nat table : Nat.below n ⊢ Nat_ /- To translate: half.match_1 (motive := fun _ => Nat) x (fun _ => 0) -- Case for 0 (fun _ => 0) -- Case for 1 (fun n => Nat.succ (half' n)) -- Case for n + 2 -/

The next step is to replace occurrences of x in the original function body with the n provided by brecOn. Because table's type depends on x, it must also be generalized when splitting cases with half.match_1, leading to a motive with an extra parameter.

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n don't know how to synthesize placeholder for argument 'h_1' context: x n : Nat table : Nat.below n ⊢ Unit → (fun k => Nat.below k → Nat) Nat.zero_ don't know how to synthesize placeholder for argument 'h_2' context: x n : Nat table : Nat.below n ⊢ Unit → (fun k => Nat.below k → Nat) 1_ don't know how to synthesize placeholder for argument 'h_3' context: x n : Nat table : Nat.below n ⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ_) table /- To translate: (fun _ => 0) -- Case for 0 (fun _ => 0) -- Case for 1 (fun n => Nat.succ (half' n)) -- Case for n + 2 -/

The three cases' placeholders expect the following types:

don't know how to synthesize placeholder for argument 'h_1'
context:
x n : Nat
table : Nat.below n
⊢ Unit → (fun k => Nat.below k → Nat) Nat.zero
don't know how to synthesize placeholder for argument 'h_2'
context:
x n : Nat
table : Nat.below n
⊢ Unit → (fun k => Nat.below k → Nat) 1
don't know how to synthesize placeholder for argument 'h_3'
context:
x n : Nat
table : Nat.below n
⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ

The first two cases in the pre-definition are constant functions, with no recursion to check:

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n (fun () _ => .zero) (fun () _ => .zero) don't know how to synthesize placeholder for argument 'h_3' context: x n : Nat table : Nat.below n ⊢ (n : Nat) → (fun k => Nat.below k → Nat) n.succ.succ_) table /- To translate: (fun n => Nat.succ (half' n)) -- Case for n + 2 -/

The final case contains a recursive call. It should be translated into a lookup into the course-of-values table. A more readable representation of the last hole's type is:

(n : Nat) Nat.below (motive := fun _ => Nat) n.succ.succ Nat

which is equivalent to

(n : Nat) Nat ×' (Nat ×' Nat.below (motive := fun _ => Nat) n) Nat

The first Nat in the course-of-values table is the result of recursion on n + 1, and the second is the result of recursion on n. The recursive call can thus be replaced by a lookup, and the elaboration is successful:

noncomputable def half'' : Nat Nat := fun (x : Nat) => x.brecOn fun n table => (half.match_1 (motive := fun k => k.below (motive := fun _ => Nat) Nat) n (fun () _ => .zero) (fun () _ => .zero) (fun _ table => Nat.succ table.2.1) table unexpected end of input; expected ')', ',' or ':'

The actual elaborator keeps track of the relationship between the parameter being checked for structural recursion and the positions in the course-of-values tables by inserting sentinel types with fresh names into the motive.

7.6.3. Well-Founded Recursion🔗

Functions defined by well-founded recursion are those in which each recursive call has arguments that are smaller (in a suitable sense) than the functions' parameters. In contrast to structural recursion, in which recursive definitions must satisfy particular syntactic requirements, definitions that use well-founded recursion employ semantic arguments. This allows a larger class of recursive definitions to be accepted. Furthermore, when Lean's automation fails to construct a termination proof, it is possible to specify one manually.

All definitions are treated identically by the Lean compiler. In Lean's logic, definitions that use well-founded recursion typically do not reduce definitionally. The reductions do hold as propositional equalities, however, and Lean automatically proves them. This does not typically make it more difficult to prove properties of definitions that use well-founded recursion, because the propositional reductions can be used to reason about the behavior of the function. It does mean, however, that using these functions in types typically does not work well. Even when the reduction behavior happens to hold definitionally, it is often much slower than structurally recursive definitions in the kernel, which must unfold the termination proof along with the definition. When possible, recursive function that are intended for use in types or in other situations where definitional equality is important should be defined with structural recursion.

To explicitly use well-founded recursion, a function or theorem definition can be annotated with a Lean.Parser.Command.declaration : commandtermination_by clause that specifies the measure by which the function terminates. The measure should be a term that decreases at each recursive call; it may be one of the function's parameters or a tuple of the parameters, but it may also be any other term. The measure's type must be equipped with a well-founded relation, which determines what it means for the measure to decrease.

syntaxExplicit Well-Founded Recursion

The Lean.Parser.Command.declaration : commandtermination_by clause introduces the termination argument.

Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferrred termination measure will be suggested.

terminationBy ::= ...
    | Specify a termination measure for recursive functions.
```
termination_by a - b
```
indicates that termination of the currently defined recursive function follows
because the difference between the arguments `a` and `b` decreases.

If the function takes further argument after the colon, you can name them as follows:
```
def example (a : Nat) : Nat → Nat → Nat :=
termination_by b c => a - b
```

By default, a `termination_by` clause will cause the function to be constructed using well-founded
recursion. The syntax `termination_by structural a` (or `termination_by structural _ c => c`)
indicates the function is expected to be structural recursive on the argument. In this case
the body of the `termination_by` clause must be one of the function's parameters.

If omitted, a termination measure will be inferred. If written as `termination_by?`,
the inferrred termination measure will be suggested.

termination_by (ident* =>)? term

The identifiers before the optional => can bring function parameters into scope that are not already bound in the declaration header, and the mandatory term must indicate one of the function's parameters, whether introduced in the header or locally in the clause.

Division by Iterated Subtraction

Division can be specified as the number of times the divisor can be subtracted from the dividend. This operation cannot be elaborated using structural recursion because subtraction is not pattern matching. The value of n does decrease with each recursive call, so well-founded recursion can be used to justify the definition of division by iterated subtraction.

def div (n k : Nat) : Nat := if k = 0 then 0 else if k > n then 0 else 1 + div (n - k) k termination_by n

7.6.3.1. Well-Founded Relations🔗

A relation is a well-founded relation if there exists no infinitely descending chain

x_0 ≻ x_1 ≻ \cdots

In Lean, types that are equipped with a canonical well-founded relation are instances of the WellFoundedRelation type class.

🔗type class
WellFoundedRelation.{u} (α : Sort u) : Sort (max 1 u)
WellFoundedRelation.{u} (α : Sort u) : Sort (max 1 u)

A type that has a standard well-founded relation.

Instances are used to prove that functions terminate using well-founded recursion by showing that recursive calls reduce some measure according to a well-founded relation. This relation can combine well-founded relations on the recursive function's parameters.

Instance Constructor

WellFoundedRelation.mk.{u}

Methods

rel : α  α  Prop

A well-founded relation on α.

wf : WellFounded WellFoundedRelation.rel

A proof that rel is, in fact, well-founded.

The most important instances are:

  • Nat, ordered by (· < ·).

  • Prod, ordered lexicographically: (a₁, b₁) (a₂, b₂) if and only if a₁ a₂ or a₁ = a₂ and b₁ b₂.

  • Every type that is an instance of the SizeOf type class, which provides a method SizeOf.sizeOf, has a well-founded relation. For these types, x₁ x₂ if and only if sizeOf x₁ < sizeOf x₂. For inductive types, a SizeOf instance is automatically derived by Lean.

Note that there exists a low-priority instance instSizeOfDefault that provides a SizeOf instance for any type, and always returns 0. This instance cannot be used to prove that a function terminates using well-founded recursion because 0 < 0 is false.

Default Size Instance

Function types in general do not have a well-founded relation that's useful for termination proofs. Instance synthesis thus selects instSizeOfDefault and the corresponding well-founded relation. If the measure is a function, the default SizeOf instance is selected and the proof cannot succeed.

def declaration uses 'sorry'fooInst (b : Bool Bool) : Unit := fooInst (b b) termination_by b decreasing_by b:Bool BoolsizeOf (b b) < sizeOf b b:Bool Bool0 < 0 b:Bool Bool0 < 0 b:Bool BoolFalse b:Bool BoolFalse All goals completed! 🐙

7.6.3.2. Termination proofs

Once a measure is specified and its well-founded relation is determined, Lean determines the termination proof obligation for every recursive call.

The proof obligation for each recursive call is of the form g a₁ a₂ g p₁ p₂ , where:

  • g is the measure as a function of the parameters,

  • is the inferred well-founded relation,

  • a₁ a₂ are the arguments of the recursive call and

  • p₁ p₂ are the parameters of the function definition.

The context of the proof obligation is the local context of the recursive call. In particular, local assumptions (such as those introduced by if h : _, match h : _ with or have) are available. If a function parameter is the discriminant of a pattern match (e.g. by a Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expression), then this parameter is refined to the matched pattern in the proof obligation.

The overall termination proof obligation consists of one goal for each recursive call. By default, the tactic decreasing_trivial is used to prove each proof obligation. A custom tactic script can be provided using the optional Lean.Parser.Command.declaration : commanddecreasing_by clause, which comes after the Lean.Parser.Command.declaration : commandtermination_by clause. This tactic script is run once, with one goal for each proof obligation, rather than separately on each proof obligation.

Termination Proof Obligations

The following recursive definition of the Fibonacci numbers has two recursive calls, which results in two goals in the termination proof.

def fib (n : Nat) := if h : n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n unsolved goals n : Nat h : ¬n ≤ 1 ⊢ n - 1 < n n : Nat h : ¬n ≤ 1 ⊢ n - 2 < ndecreasing_by n:Nath:¬n 1n - 1 < nn:Nath:¬n 1n - 2 < n
n:Nath:¬n 1n - 1 < nn:Nath:¬n 1n - 2 < n

Here, the measure is simply the parameter itself, and the well-founded order is the less-than relation on natural numbers. The first proof goal requires the user to prove that the argument of the first recursive call, namely n - 1, is strictly smaller than the function's parameter, n.

Both termination proofs can be easily discharged using the omega tactic.

def fib (n : Nat) := if h : n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n decreasing_by n:Nath:¬n 1n - 1 < n All goals completed! 🐙 n:Nath:¬n 1n - 2 < n All goals completed! 🐙
Refined Parameters

If a parameter of the function is the discriminant of a pattern match, then the proof obligations mention the refined parameter.

def fib : Nat Nat | 0 | 1 => 1 | .succ (.succ n) => fib (n + 1) + fib n termination_by n => n unsolved goals n : Nat ⊢ n + 1 < n.succ.succ n : Nat ⊢ n < n.succ.succdecreasing_by n:Natn + 1 < n.succ.succn:Natn < n.succ.succ
n:Natn + 1 < n.succ.succn:Natn < n.succ.succ

Additionally, the context is enriched with additional assumptions that can make it easier to prove termination. Some examples include:

  • In the branches of an if-then-else expression, a hypothesis that asserts the current branch's condition is added, much as if the dependent if-then-else syntax had been used.

  • In the function argument to certain higher-order functions, the context of the function's body is enriched with assumptions about the argument.

This list is not exhaustive, and the mechanism is extensible. It is described in detail in the section on preprocessing.

Enriched Proof Obligation Contexts

Here, the termIfThenElse : term`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to return `t` or `e` depending on whether `c` is true or false. The explicit argument `c : Prop` does not have any actual computational content, but there is an additional `[Decidable c]` argument synthesized by typeclass inference which actually determines how to evaluate `c` to true or false. Write `if h : c then t else e` instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact that `c` is true/false. if does not add a local assumption about the condition (that is, whether n 1) to the local contexts in the branches.

def fib (n : Nat) := if n 1 then 1 else fib (n - 1) + fib (n - 2) termination_by n unsolved goals n : Nat h✝ : ¬n ≤ 1 ⊢ n - 1 < n n : Nat h✝ : ¬n ≤ 1 ⊢ n - 2 < ndecreasing_by n:Nath✝:¬n 1n - 1 < nn:Nath✝:¬n 1n - 2 < n

Nevertheless, the assumptions are available in the context of the termination proof:

n:Nath✝:¬n 1n - 1 < nn:Nath✝:¬n 1n - 2 < n

Termination proof obligations in body of a Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates of the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `ToStream` typeclass. forLean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates of the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `ToStream` typeclass. in loop are also enriched, in this case with a Std.Range membership hypothesis:

def f (xs : Array Nat) : Nat := Id.run do let mut s := xs.sum for i in [:xs.size] do s := s + f (xs.take i) pure s termination_by xs unsolved goals xs : Array Nat i : Nat h✝ : i ∈ { stop := xs.size, step_pos := Nat.zero_lt_one } ⊢ sizeOf (xs.take i) < sizeOf xsdecreasing_by xs:Array Nati:Nath✝:i { stop := xs.size, step_pos := Nat.zero_lt_one }sizeOf (xs.take i) < sizeOf xs
xs:Array Nati:Nath✝:i { stop := xs.size, step_pos := Nat.zero_lt_one }sizeOf (xs.take i) < sizeOf xs

Similarly, in the following (contrived) example, the termination proof contains an additional assumption showing that x xs.

def f (n : Nat) (xs : List Nat) : Nat := List.sum (xs.map (fun x => f x [])) termination_by xs unsolved goals n : Nat xs : List Nat x : Nat h✝ : x ∈ xs ⊢ sizeOf [] < sizeOf xsdecreasing_by n:Natxs:List Natx:Nath✝:x xssizeOf [] < sizeOf xs
n:Natxs:List Natx:Nath✝:x xssizeOf [] < sizeOf xs

This feature requires special setup for the higher-order function under which the recursive call is nested, as described in the section on preprocessing. In the following definition, identical to the one above except using a custom, equivalent function instead of List.map, the proof obligation context is not enriched:

def List.myMap := @List.map def f (n : Nat) (xs : List Nat) : Nat := List.sum (xs.myMap (fun x => f x [])) termination_by xs unsolved goals n : Nat xs : List Nat x : Nat ⊢ sizeOf [] < sizeOf xsdecreasing_by n:Natxs:List Natx:NatsizeOf [] < sizeOf xs
n:Natxs:List Natx:NatsizeOf [] < sizeOf xs

7.6.3.3. Default Termination Proof Tactic

If no Lean.Parser.Command.declaration : commanddecreasing_by clause is given, then the decreasing_tactic is used implicitly, and applied to each proof obligation separately.

🔗tactic
decreasing_tactic

The tactic decreasing_tactic mainly deals with lexicographic ordering of tuples, applying Prod.Lex.right if the left components of the product are definitionally equal, and Prod.Lex.left otherwise. After preprocessing tuples this way, it calls the decreasing_trivial tactic.

🔗tactic
decreasing_trivial

Extensible helper tactic for decreasing_tactic. This handles the "base case" reasoning after applying lexicographic order lemmas. It can be extended by adding more macro definitions, e.g.

macro_rules | `(tactic| decreasing_trivial) => `(tactic| linarith)

The tactic decreasing_trivial is an extensible tactic that applies a few common heuristics to solve a termination goal. In particular, it tries the following tactics and theorems:

  • simp_arith

  • assumption

  • theorems Nat.sub_succ_lt_self, Nat.pred_lt_of_lt, and Nat.pred_lt, which handle common arithmetic goals

  • omega

  • array_get_dec and array_mem_dec, which prove that the size of array elements is less than the size of the array

  • sizeOf_list_dec that the size of list elements is less than the size of the list

  • String.Iterator.sizeOf_next_lt_of_hasNext and String.Iterator.sizeOf_next_lt_of_atEnd, to handle iteration through a string using Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass. `break` and `continue` are supported inside `for` loops. `for x in e, x2 in e2, ... do s` iterates of the given collections in parallel, until at least one of them is exhausted. The types of `e2` etc. must implement the `ToStream` typeclass. for

This tactic is intended to be extended with further heuristics using Lean.Parser.Command.macro_rules : commandmacro_rules.

No Backtracking of Lexicographic Order

A classic example of a recursive function that needs a more complex measure is the Ackermann function:

def ack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => ack m 1 | m + 1, n + 1 => ack m (ack (m + 1) n) termination_by m n => (m, n)

The measure is a tuple, so every recursive call has to be on arguments that are lexicographically smaller than the parameters. The default decreasing_tactic can handle this.

In particular, note that the third recursive call has a second argument that is smaller than the second parameter and a first argument that is definitionally equal to the first parameter. This allowed decreasing_tactic to apply Prod.Lex.right.

Prod.Lex.right {α β} {ra : α α Prop} {rb : β β Prop} (a : α) {b₁ b₂ : β} (h : rb b₁ b₂) : Prod.Lex ra rb (a, b₁) (a, b₂)

It fails, however, with the following modified function definition, where the third recursive call's first argument is provably smaller or equal to the first parameter, but not syntactically equal:

def synack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => synack m 1 | m + 1, n + 1 => synack m (failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal case h m n : Nat ⊢ m / 2 + 1 < m + 1synack (m / 2 + 1) n) termination_by m n => (m, n)
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
case h
m n : Nat
⊢ m / 2 + 1 < m + 1

Because Prod.Lex.right is not applicable, the tactic used Prod.Lex.left, which resulted in the unprovable goal above.

This function definition may require a manual proof that uses the more general theorem Prod.Lex.right', which allows the first component of the tuple (which must be of type Nat) to be less or equal instead of strictly equal:

Prod.Lex.right' {β} (rb : β β Prop) {a₂ : Nat} {b₂ : β} {a₁ : Nat} {b₁ : β} (h₁ : a₁ a₂) (h₂ : rb b₁ b₂) : Prod.Lex Nat.lt rb (a₁, b₁) (a₂, b₂)def synack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => synack m 1 | m + 1, n + 1 => synack m (synack (m / 2 + 1) n) termination_by m n => (m, n) decreasing_by m:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, 1) (m.succ, 0) m:Natm < m.succ All goals completed! 🐙 -- the next goal corresponds to the third recursive call m:Natn:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m / 2 + 1, n) (m.succ, n.succ) m:Natn:Natm / 2 + 1 m.succm:Natn:Natn < n.succ m:Natn:Natm / 2 + 1 m.succ All goals completed! 🐙 m:Natn:Natn < n.succ All goals completed! 🐙 m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y m.succ, n.succ NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (m, x✝ m / 2 + 1, n ) (m.succ, n.succ) m:Natn:Natx✝:(y : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y m.succ, n.succ Natm < m.succ All goals completed! 🐙

The decreasing_tactic tactic does not use the stronger Prod.Lex.right' because it would require backtracking on failure.

7.6.3.4. Inferring Well-Founded Recursion🔗

If a recursive function definition does not indicate a termination measure, Lean will attempt to discover one automatically. If neither Lean.Parser.Command.declaration : commandtermination_by nor Lean.Parser.Command.declaration : commanddecreasing_by is provided, Lean will try to infer structural recursion before attempting well-founded recursion. If a Lean.Parser.Command.declaration : commanddecreasing_by clause is present, only well-founded recursion is attempted.

To infer a suitable termination measure, Lean considers multiple basic termination measures, which are termination measures of type Nat, and then tries all tuples of these measures.

The basic termination measures considered are:

  • all parameters whose type have a non-trivial SizeOf instance

  • the expression e₂ - e₁ whenever the local context of a recursive call has an assumption of type e₁ < e₂ or e₁ ≤ e₂, where e₁ and e₂ are of type Nat and depend only on the function's parameters. This approach is based on work by Panagiotis Manolios and Daron Vroon, 2006. “Termination Analysis with Calling Context Graphs”. In Proceedings of the International Conference on Computer Aided Verification (CAV 2006). (LNCS 4144).

  • in a mutual group, an additional basic measure is used to distinguish between recursive calls to other functions in the group and recursive calls to the function being defined (for details, see the section on mutual well-founded recursion)

Candidate measures are basic measures or tuples of basic measures. If any of the candidate measures allow all proof obligations to be discharged by the termination proof tactic (that is, the tactic specified by Lean.Parser.Command.declaration : commanddecreasing_by, or decreasing_trivial if there is no Lean.Parser.Command.declaration : commanddecreasing_by clause), then an arbitrary such candidate measure is selected as the automatic termination measure.

A termination_by? clause causes the inferred termination annotation to be shown. It can be automatically added to the source file using the offered suggestion or code action.

To avoid the combinatorial explosion of trying all tuples of measures, Lean first tabulates all basic termination measures, determining whether the basic measure is decreasing, strictly decreasing, or non-decreasing. A decreasing measure is smaller for at least one recursive call and never increases at any recursive call, while a strictly decreasing measure is smaller at all recursive calls. A non-decreasing measure is one that the termination tactic could not show to be decreasing or strictly decreasing. A suitable tuple is chosen based on the table.This approach is based on Lukas Bulwahn, Alexander Krauss, and Tobias Nipkow, 2007. “Finding Lexicographic Orders for Termination Proofs in Isabelle/HOL”. In Proceedings of the International Conference on Theorem Proving in Higher Order Logics (TPHOLS 2007). (LNTCS 4732). This table shows up in the error message when no automatic measure could be found.

Termination failure

If there is no Lean.Parser.Command.declaration : commandtermination_by clause, Lean attempts to infer a measure for well-founded recursion. If it fails, then it prints the table mentioned above. In this example, the Lean.Parser.Command.declaration : commanddecreasing_by clause simply prevents Lean from also attempting structural recursion; this keeps the error message specific.

Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) n m l 1) 32:6-25 = = = 2) 33:6-23 = < _ 3) 34:6-23 < _ _ Please use `termination_by` to specify a decreasing measure.def f : (n m l : Nat) Nat | n+1, m+1, l+1 => [ f (n+1) (m+1) (l+1), f (n+1) (m-1) (l), f (n) (m+1) (l) ].sum | _, _, _ => 0 decreasing_by all_goals decreasing_tactic
Could not find a decreasing measure.
The basic measures relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
           n m l
1) 32:6-25 = = =
2) 33:6-23 = < _
3) 34:6-23 < _ _
Please use `termination_by` to specify a decreasing measure.

The three recursive calls are identified by their source positions. This message conveys the following facts:

  • In the first recursive call, all arguments are (provably) equal to the parameters

  • In the second recursive call, the first argument is equal to the first parameter and the second argument is provably smaller than the second parameter. The third parameter was not checked for this recursive call, because it was not necessary to determine that no suitable termination argument exists.

  • In the third recursive call, the first argument decreases strictly, and the other arguments were not checked.

When termination proofs fail in this manner, a good technique to discover the problem is to explicitly indicate the expected termination argument using Lean.Parser.Command.declaration : commandtermination_by. This will surface the messages from the failing tactic.

Array Indexing

The purpose of considering expressions of the form e₂ - e₁ as measures is to support the common idiom of counting up to some upper bound, in particular when traversing arrays in possibly interesting ways. In the following function, which performs binary search on a sorted array, this heuristic helps Lean to find the j - i measure.

def binarySearch (x : Int) (xs : Array Int) : Option Nat := go 0 xs.size where go (i j : Nat) (hj : j xs.size := by omega) := if h : i < j then let mid := (i + j) / 2 let y := xs[mid] if x = y then some mid else if x < y then go i mid else go (mid + 1) j else none Try this: termination_by (j, j - i)termination_by?

The fact that the inferred termination argument uses some arbitrary measure, rather than an optimal or minimal one, is visible in the inferred measure, which contains a redundant j:

Try this: termination_by (j, j - i)
Termination Proof Tactics During Inference

The tactic indicated by Lean.Parser.Command.declaration : commanddecreasing_by is used slightly differently when inferring the termination measure than it is in the actual termination proof.

  • During inference, it is applied to a single goal, attempting to prove < or on Nat.

  • During the termination proof, it is applied to many simultaneous goals (one per recursive call), and the goals may involve the lexicographic ordering of pairs.

A consequence is that a Lean.Parser.Command.declaration : commanddecreasing_by block that addresses goals individually and which works successfully with an explicit termination argument can cause inference of the termination measure to fail:

Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) x1 x2 1) 631:16-23 ? ? 2) 632:27-40 _ _ 3) 632:20-41 _ _ Please use `termination_by` to specify a decreasing measure.def ack : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => ack m 1 | m + 1, n + 1 => ack m (ack (m + 1) n) decreasing_by · apply Prod.Lex.left omega · apply Prod.Lex.right omega · apply Prod.Lex.left omega

It is advisable to always include a Lean.Parser.Command.declaration : commandtermination_by clause whenever an explicit Lean.Parser.Command.declaration : commanddecreasing_by proof is given.

Inference too powerful

Because decreasing_tactic avoids the need to backtrack by being incomplete with regard to lexicographic ordering, Lean may infer a termination measure that leads to goals that the tactic cannot prove. In this case, the error message is the one that results from the failing tactic rather than the one that results from being unable to find a measure. This is what happens in notAck:

def notAck : Nat Nat Nat | 0, n => n + 1 | m + 1, 0 => notAck m 1 | m + 1, n + 1 => notAck m (notAck (m / 2 + 1) n) decreasing_by all_goals failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal case h m n : Nat ⊢ m / 2 + 1 < m + 1All goals completed! 🐙
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
case h
m n : Nat
⊢ m / 2 + 1 < m + 1

In this case, explicitly stating the termination measure helps.

7.6.3.5. Mutual Well-Founded Recursion🔗

Lean supports the definition of mutually recursive functions using well-founded recursion. Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : termlet rec expressions and Lean.Parser.Command.declaration : commandwhere blocks. The rules for mutual well-founded recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups.

If any function in the mutual group has a Lean.Parser.Command.declaration : commandtermination_by or Lean.Parser.Command.declaration : commanddecreasing_by clause, well-founded recursion is attempted. If a termination measure is specified using Lean.Parser.Command.declaration : commandtermination_by for any function in the mutual group, then all functions in the group must specify a termination measure, and they have to have the same type.

If no termination argument is specified, the termination argument is inferred, as described above. In the case of mutual recursion, a third class of basic measures is considered during inference, namely for each function in the mutual group the measure that is 1 for that function and 0 for the others. This allows Lean to order the functions so that some calls from one function to another are allowed even if the parameters do not decrease.

Mutual recursion without parameter decrease

In the following mutual function definitions, the parameter does not decrease in the call from g to f. Nonetheless, the definition is accepted due to the ordering imposed on the functions themselves by the additional basic measure.

mutual def f : (n : Nat) Nat | 0 => 0 | n+1 => g n Try this: termination_by n => (n, 0)termination_by? def g (n : Nat) : Nat := (f n) + 1 Try this: termination_by (n, 1)termination_by? end

The inferred termination argument for f is:

Try this: termination_by n => (n, 0)

The inferred termination argument for g is:

Try this: termination_by (n, 1)

7.6.3.6. Preprocessing Function Definitions🔗

Lean preprocesses the function's body before determining the proof obligations at each call site, transforming it into an equivalent definition that may include additional information. This preprocessing step is primarily used to enrich the local context with additional assumptions that may be necessary in order to solve the termination proof obligations, freeing users from the need to perform equivalent transformations by hand. Preprocessing uses the simplifier and is extensible by the user.

The preprocessing happens in three steps:

  1. Lean annotates occurrences of a function's parameter, or a subterm of a parameter, with the wfParam gadget.

    wfParam {α} (a : α) : α

    More precisely, every occurrence of the function's parameters is wrapped in wfParam. Whenever a Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expression has any discriminant wrapped in wfParam, the gadget is removed and every occurrence of a pattern match variable (regardless of whether it comes from the discriminant with the wfParam gadget) is wrapped in wfParam. The wfParam gadget is additionally floated out of projection function applications.

  2. The annotated function body is simplified using the simplifier, using only simplification rules from the wf_preprocess custom simp set.

  3. Finally, any left-over wfParam markers are removed.

Annotating function parameters that are used for well-founded recursion allows the preprocessing simplification rules to distinguish between parameters and other terms.

attributePreprocessing Simp Set for Well-Founded Recursion
attr ::= ...
    | Theorems tagged with the `wf_preprocess` attribute are used during the processing of functions defined
by well-founded recursion. They are applied to the function's body to add additional hypotheses,
such as replacing `if c then _ else _` with `if h : c then _ else _` or `xs.map` with
`xs.attach.map`. Also see `wfParam`.
wf_preprocess

Theorems tagged with the wf_preprocess attribute are used during the processing of functions defined by well-founded recursion. They are applied to the function's body to add additional hypotheses, such as replacing if c then _ else _ with if h : c then _ else _ or xs.map with xs.attach.map. Also see wfParam.

🔗def
wfParam.{u} {α : Sort u} (a : α) : α
wfParam.{u} {α : Sort u} (a : α) : α

The wfParam gadget is used internally during the construction of recursive functions by wellfounded recursion, to keep track of the parameter for which the automatic introduction of List.attach (or similar) is plausible.

Some rewrite rules in the wf_preprocess simp set apply generally, without heeding the wfParam marker. In particular, the theorem ite_eq_dite is used to extend the context of an if-then-else expression branch with an assumption about the condition:This assumption's name should be an inaccessible name based on h, as is indicated by using binderNameHint with the term (). Binder name hints are described in the tactic language reference.

ite_eq_dite {P : Prop} {α : Sort u} {a b : α} [Decidable P] : (if P then a else b) = if h : P then binderNameHint h () a else binderNameHint h () b

Other rewrite rules use the wfParam marker to restrict their applicability; they are used only when a function (like List.map) is applied to a parameter or subterm of a parameter, but not otherwise. This is typically done in two steps:

  1. A theorem such as List.map_wfParam recognizes a call of List.map on a function parameter (or subterm), and uses List.attach to enrich the type of the list elements with the assertion that they are indeed elements of that list:

    List.map_wfParam (xs : List α) (f : α β) : (wfParam xs).map f = xs.attach.unattach.map f
  2. A theorem such as List.map_unattach makes that assertion available to the function parameter of List.map.

    List.map_unattach (P : α Prop) (xs : List { x : α // P x }) (f : α β) : xs.unattach.map f = xs.map fun x, h => binderNameHint x f <| binderNameHint h () <| f (wfParam x)

    This theorem uses the binderNameHint gadget to preserve a user-chosen binder name, should f be a lambda expression.

By separating the introduction of List.attach from the propagation of the introduced assumption, the desired the x xs assumption is made available to f even in chains such as (xs.reverse.filter p).map f.

This preprocessing can be disabled by setting the option wf.preprocess to false. To see the preprocessed function definition, before and after the removal of wfParam markers, set the option trace.Elab.definition.wf to true.

🔗option
trace.Elab.definition.wf

Default value: false

enable/disable tracing for the given module and submodules

Preprocessing for a custom data type

This example demonstrates what is necessary to enable automatic well-founded recursion for a custom container type. The structure type Pair is a homogeneous pair: it contains precisely two elements, both of which have the same type. It can be thought of as being similar to a list or array that always contains precisely two elements.

As a container, Pair can support a map operation. To support well-founded recursion in which recursive calls occur in the body of a function being mapped over a Pair, some additional definitions are required, including a membership predicate, a theorem that relates the size of a member to the size of the containing pair, helpers to introduce and eliminate assumptions about membership, wf_preprocess rules to insert these helpers, and an extension to the decreasing_trivial tactic. Each of these steps makes it easier to work with Pair, but none are strictly necessary; there's no need to immediately implement all steps for every type.

/-- A homogeneous pair -/ structure Pair (α : Type u) where fst : α snd : α /-- Mapping a function over the elements of a pair -/ def Pair.map (f : α β) (p : Pair α) : Pair β where fst := f p.fst snd := f p.snd

Defining a nested inductive data type of binary trees that uses Pair and attempting to define its map function demonstrates the need for preprocessing rules.

/-- A binary tree defined using `Pair` -/ inductive Tree (α : Type u) where | leaf : α Tree α | node : Pair (Tree α) Tree α

A straightforward definition of the map function fails:

def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.map (fun t' => failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal α : Type u_1 p : Pair (Tree α) t' : Tree α ⊢ sizeOf t' < 1 + sizeOf pt'.map f)) termination_by t => t
failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
α : Type u_1
p : Pair (Tree α)
t' : Tree α
⊢ sizeOf t' < 1 + sizeOf p

Clearly the proof obligation is not solvable, because nothing connects t' to p.

The standard idiom to enable this kind of function definition is to have a function that enriches each element of a collection with a proof that they are, in fact, elements of the collection. Stating this property requires a membership predicate.

inductive Pair.Mem (p : Pair α) : α Prop where | fst : Mem p p.fst | snd : Mem p p.snd instance : Membership α (Pair α) where mem := Pair.Mem

Every inductive type automatically has a SizeOf instance. An element of a collection should be smaller than the collection, but this fact must be proved before it can be used to construct a termination proof:

theorem Pair.sizeOf_lt_of_mem {α} [SizeOf α] {p : Pair α} {x : α} (h : x p) : sizeOf x < sizeOf p := α:Type u_1inst✝:SizeOf αp:Pair αx:αh:x psizeOf x < sizeOf p α:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.fst < sizeOf pα:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.snd < sizeOf p α:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.fst < sizeOf pα:Type u_1inst✝:SizeOf αp:Pair αsizeOf p.snd < sizeOf p α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:αsizeOf { fst := fst✝, snd := snd✝ }.snd < sizeOf { fst := fst✝, snd := snd✝ } α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:αsizeOf { fst := fst✝, snd := snd✝ }.fst < sizeOf { fst := fst✝, snd := snd✝ }α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:αsizeOf { fst := fst✝, snd := snd✝ }.snd < sizeOf { fst := fst✝, snd := snd✝ } (α:Type u_1inst✝:SizeOf αfst✝:αsnd✝:α0 < 1 + sizeOf fst✝; All goals completed! 🐙)

The next step is to define attach and unattach functions that enrich the elements of the pair with a proof that they are elements of the pair, or remove said proof. Here, the type of Pair.unattach is more general and can be used with any subtype; this is a typical pattern.

def Pair.attach (p : Pair α) : Pair {x : α // x p} where fst := p.fst, .fst snd := p.snd, .snd def Pair.unattach {P : α Prop} : Pair {x : α // P x} Pair α := Pair.map Subtype.val

Tree.map can now be defined by using Pair.attach and Pair.sizeOf_lt_of_mem explicitly:

def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.attach.map (fun t', _ => t'.map f)) termination_by t => t decreasing_by α:Type u_1p:Pair (Tree α)t':Tree αproperty✝:t' pthis:sizeOf t' < sizeOf psizeOf t' < sizeOf (node p) α:Type u_1p:Pair (Tree α)t':Tree αproperty✝:t' pthis:sizeOf t' < sizeOf psizeOf t' sizeOf p All goals completed! 🐙

This transformation can be made fully automatic. The preprocessing feature of well-founded recursion can be used to automate the introduction of the Pair.attach function. This is done in two stages. First, when Pair.map is applied to one of the function's parameters, it is rewritten to an attach/unattach composition. Then, when a function is mapped over the result of Pair.unattach, the function is rewritten to accept the proof of membership and bring it into scope.

@[wf_preprocess] theorem Pair.map_wfParam (f : α β) (p : Pair α) : (wfParam p).map f = p.attach.unattach.map f := α:Type u_1β:Type u_2f:α βp:Pair αmap f (wfParam p) = map f p.attach.unattach α:Type u_1β:Type u_2f:α βfst✝:αsnd✝:αmap f (wfParam { fst := fst✝, snd := snd✝ }) = map f { fst := fst✝, snd := snd✝ }.attach.unattach All goals completed! 🐙 @[wf_preprocess] theorem Pair.map_unattach {P : α Prop} (p : Pair (Subtype P)) (f : α β) : p.unattach.map f = p.map fun x, h => binderNameHint x f <| f (wfParam x) := α:Type u_1β:Type u_2P:α Propp:Pair (Subtype P)f:α βmap f p.unattach = map (fun x => match x with | x, h => binderNameHint x f (f (wfParam x))) p α:Type u_1β:Type u_2P:α Propf:α βfst✝:Subtype Psnd✝:Subtype Pmap f { fst := fst✝, snd := snd✝ }.unattach = map (fun x => match x with | x, h => binderNameHint x f (f (wfParam x))) { fst := fst✝, snd := snd✝ }; All goals completed! 🐙

Now the function body can be written without extra considerations, and the membership assumption is still available to the termination proof.

def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.map (fun t' => t'.map f)) termination_by t => t decreasing_by α:Type u_1p:Pair (Tree α)t':Tree αh:t' pthis:sizeOf t' < sizeOf psizeOf t' < sizeOf (node p) α:Type u_1p:Pair (Tree α)t':Tree αh:t' pthis:sizeOf t' < sizeOf psizeOf t' < 1 + sizeOf p All goals completed! 🐙

The proof can be made fully automatic by adding sizeOf_lt_of_mem to the decreasing_trivial tactic, as is done for similar built-in theorems.

macro "sizeOf_pair_dec" : tactic => `(tactic| with_reducible have := Pair.sizeOf_lt_of_mem _ omega done) macro_rules | `(tactic| decreasing_trivial) => `(tactic| sizeOf_pair_dec) def Tree.map (f : α β) : Tree α Tree β | leaf x => leaf (f x) | node p => node (p.map (fun t' => t'.map f)) termination_by t => t

To keep the example short, the sizeOf_pair_dec tactic is tailored to this particular recursion pattern and isn't really general enough for a general-purpose container library. It does, however, demonstrate that libraries can be just as convenient in practice as the container types in the standard library.

7.6.3.7. Theory and Construction

This section gives a very brief glimpse into the mathematical constructions that underlie termination proofs via well-founded recursion, which may surface occasionally. The elaboration of functions defined by well-founded recursion is based on the WellFounded.fix operator.

🔗def
WellFounded.fix.{u, v} {α : Sort u} {C : α Sort v} {r : α α Prop} (hwf : WellFounded r) (F : (x : α) ((y : α) r y x C y) C x) (x : α) : C x
WellFounded.fix.{u, v} {α : Sort u} {C : α Sort v} {r : α α Prop} (hwf : WellFounded r) (F : (x : α) ((y : α) r y x C y) C x) (x : α) : C x

A well-founded fixpoint. If satisfying the motive C for all values that are smaller according to a well-founded relation allows it to be satisfied for the current value, then it is satisfied for all values.

This function is used as part of the elaboration of well-founded recursion.

The type α is instantiated with the function's (varying) parameters, packed into one type using PSigma. The WellFounded relation is constructed from the termination measure via invImage.

🔗def
invImage.{u_1, u_2} {α : Sort u_1} {β : Sort u_2} (f : α β) (h : WellFoundedRelation β) : WellFoundedRelation α
invImage.{u_1, u_2} {α : Sort u_1} {β : Sort u_2} (f : α β) (h : WellFoundedRelation β) : WellFoundedRelation α

The inverse image of a well-founded relation is well-founded.

The function's body is passed to WellFounded.fix, with parameters suitably packed and unpacked, and recursive calls are replaced with a call to the value provided by WellFounded.fix. The termination proofs generated by the Lean.Parser.Command.declaration : commanddecreasing_by tactics are inserted in the right place.

Finally, the equational and unfolding theorems for the recursive function are proved from WellFounded.fix_eq. These theorems hide the details of packing and unpacking arguments and describe the function's behavior in terms of the original definition.

In the case of mutual recursion, an equivalent non-mutual function is constructed by combining the function's arguments using PSum, and pattern-matching on that sum type in the result type and the body.

The definition of WellFounded builds on the notion of accessible elements of the relation:

🔗inductive predicate
WellFounded.{u} {α : Sort u} (r : α α Prop) : Prop
WellFounded.{u} {α : Sort u} (r : α α Prop) : Prop

A relation r is WellFounded if all elements of α are accessible within r. If a relation is WellFounded, it does not allow for an infinite descent along the relation.

If the arguments of the recursive calls in a function definition decrease according to a well founded relation, then the function terminates. Well-founded relations are sometimes called Artinian or said to satisfy the “descending chain condition”.

Constructors

intro.{u} {α : Sort u} {r : α  α  Prop}
  (h :  (a : α), Acc r a) : WellFounded r

If all elements are accessible via r, then r is well-founded.

🔗inductive predicate
Acc.{u} {α : Sort u} (r : α α Prop) : α Prop
Acc.{u} {α : Sort u} (r : α α Prop) : α Prop

Acc is the accessibility predicate. Given some relation r (e.g. <) and a value x, Acc r x means that x is accessible through r:

x is accessible if there exists no infinite sequence ... < y₂ < y₁ < y₀ < x.

Constructors

intro.{u} {α : Sort u} {r : α  α  Prop} (x : α)
  (h :  (y : α), r y x  Acc r y) : Acc r x

A value is accessible if for all y such that r y x, y is also accessible. Note that if there exists no y such that r y x, then x is accessible. Such an x is called a base case.

Division by Iterated Subtraction: Termination Proof

The definition of division by iterated subtraction can be written explicitly using well-founded recursion.

noncomputable def div (n k : Nat) : Nat := (inferInstanceAs (WellFoundedRelation Nat)).wf.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + (r (n - k) <| α:Type un✝:Natk:Natn:Natr:(y : Nat) WellFoundedRelation.rel y n Nath✝:¬k = 0h:¬k > nWellFoundedRelation.rel (n - k) n α:Type un✝:Natk:Natn:Natr:(y : Nat) WellFoundedRelation.rel y n Nath✝:¬k = 0h:¬k > nn - k < n All goals completed! 🐙)) n

The definition must be marked Lean.Parser.Command.declaration : commandnoncomputable because well-founded recursion is not supported by the compiler. Like recursors, it is part of Lean's logic.

The definition of division should satisfy the following equations:

  • {n k : Nat}, (k = 0) div n k = 0

  • {n k : Nat}, (k > n) div n k = 0

  • {n k : Nat}, (k 0) (¬ k > n) div n k = div (n - k) k

This reduction behavior does not hold definitionally:

theorem div.eq0 : div n 0 = 0 := n:Natdiv n 0 = 0 tactic 'rfl' failed, the left-hand side div n 0 is not definitionally equal to the right-hand side 0 n : Nat ⊢ div n 0 = 0n:Natdiv n 0 = 0
tactic 'rfl' failed, the left-hand side
  div n 0
is not definitionally equal to the right-hand side
  0
n : Nat
⊢ div n 0 = 0

However, using WellFounded.fix_eq to unfold the well-founded recursion, the three equations can be proved to hold:

theorem div.eq0 : div n 0 = 0 := n:Natdiv n 0 = 0 n:Nat_proof_2.fix (fun n r => if h : 0 = 0 then 0 else if h : 0 > n then 0 else 1 + r (n - 0) ) n = 0 All goals completed! 🐙 theorem div.eq1 : k > n div n k = 0 := k:Natn:Natk > n div n k = 0 k:Natn:Nath:k > ndiv n k = 0 k:Natn:Nath:k > n_proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) n = 0 k:Natn:Nath:k > n(if h : k = 0 then 0 else if h : k > n then 0 else 1 + (fun y x => _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) y) (n - k) ) = 0 k:Natn:Nath:k > n¬k = 0 k n 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ) (n - k) = 0 k:Natn:Nath:k > na✝¹:¬k = 0a✝:k n1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : n < k then 0 else 1 + r (n - k) ) (n - k) = 0; All goals completed! 🐙 theorem div.eq2 : ¬ k = 0 ¬ (k > n) div n k = 1 + div (n - k) k := k:Natn:Nat¬k = 0 ¬k > n div n k = 1 + div (n - k) k k:Natn:Nata✝¹:¬k = 0a✝:¬k > ndiv n k = 1 + div (n - k) k k:Natn:Nata✝¹:¬k = 0a✝:¬k > n_proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) n = 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) (n - k) k:Natn:Nata✝¹:¬k = 0a✝:¬k > n(if h : k = 0 then 0 else if h : k > n then 0 else 1 + (fun y x => _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) y) (n - k) ) = 1 + _proof_2.fix (fun n r => if h : k = 0 then 0 else if h : k > n then 0 else 1 + r (n - k) ) (n - k) k:Natn:Nata✝¹:¬k = 0a✝:k nn < k 0 = 1 + _proof_2.fix (fun n r => if h : n < k then 0 else 1 + r (n - k) ) (n - k) All goals completed! 🐙

7.6.4. Partial Fixpoint Recursion🔗

All definitions are fundamentally equations: the new constant being defined is equal to the right-hand side of the definition. For functions defined by structural recursion, this equation holds definitionally, and there is a unique value returned by application of the function. For functions defined by well-founded recursion, the equation may hold only propositionally, but all type-correct applications of the function to arguments are equal to the respective values prescribed by the definition. In both cases, the fact that the function terminates for all inputs means that the value computed by applying the function is always uniquely determined.

In some cases where a function does not terminate for all arguments, the equation may not uniquely determine the function's return value for each input, but there are nonetheless functions for which the defining equation holds. In these cases, a definition as a partial fixpoint may still be possible. Any function that satisfies the defining equation can be used to demonstrate that the equation does not create a logical contradiction, and the equation can then be proven as a theorem about this function. As with the other strategies for defining recursive functions, compiled code uses the function as it was originally written; like definitions in terms of eliminators or recursion over accessibility proofs, the function used to define the partial fixpoint is used only to justify the function's equations in Lean's logic for purposes of mathematical reasoning.

The term partial fixpoint is specific to Lean. Functions declared Lean.Parser.Command.declaration : commandpartial do not require termination proofs, so long as the type of their return values is inhabited, but they are completely opaque from the perspective of Lean's logic. Partial fixpoints, on the other hand, can be rewritten using their defining equations while writing proofs. Logically speaking, partial fixpoints are total functions that don't reduce definitionally when applied, but for which equational rewrite rule are provided. They are partial in the sense that the defining equation does not necessarily specify a value for all possible arguments.

While partial fixpoints do allow functions to be defined that cannot be expressed using structural or well-founded recursion, the technique is also useful in other cases. Even in cases where the defining equation fully describes the function's behavior and a termination proof using well-founded recursion would be possible, it may simply be more convenient to define the function as a partial fixpoint to avoid a having to write a termination proof.

Defining recursive functions as partial fixpoints only occurs when explicitly requested by annotating the definition with Lean.Parser.Command.declaration : commandpartial_fixpoint.

There are two classes of functions that can be defined as partial fixpoints:

  • Tail-recursive functions whose return type is inhabited type

  • Functions that return values in a suitable monad, such as the Option monad

Both classes are backed by the same theory and construction: least fixpoints of monotone equations in chain-complete partial orders.

Just as with structural and well-founded recursion, Lean allows mutually recursive functions to be defined as partial fixpoints. To use this feature, every function definition in a mutual block must be annotated with the Lean.Parser.Command.declaration : commandpartial_fixpoint modifier.

Definition by Partial Fixpoint

The following function finds the least natural number for which the predicate p holds. If p never holds, then this equation does not specify the behavior: the function find could return 42 or any other Nat in that case and still satisfy the equation.

def find (p : Nat Bool) (i : Nat := 0) : Nat := if p i then i else find p (i + 1) partial_fixpoint

The elaborator can prove that functions satisfying the equation exist. Within Lean's logic, find is defined to be an arbitrary such function.

7.6.4.1. Tail-Recursive Functions🔗

A recursive function can be defined as a partial fixpoint if the following two conditions hold:

  1. The function's return type is inhabited (as with functions marked Lean.Parser.Command.declaration : commandpartial) - either a Nonempty or Inhabited instance works.

  2. All recursive calls are in tail position of the function.

An expression is in tail position in the function body if it is:

  • the function body itself,

  • the branches of a Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expression that is in tail position,

  • the branches of an termIfThenElse : term`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to return `t` or `e` depending on whether `c` is true or false. The explicit argument `c : Prop` does not have any actual computational content, but there is an additional `[Decidable c]` argument synthesized by typeclass inference which actually determines how to evaluate `c` to true or false. Write `if h : c then t else e` instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact that `c` is true/false. if expression that is in tail position, and

  • the body of a Lean.Parser.Term.let : term`let` is used to declare a local definition. Example: ``` let x := 1 let y := x + 1 x + y ``` Since functions are first class citizens in Lean, you can use `let` to declare local functions too. ``` let double := fun x => 2*x double (double 3) ``` For recursive definitions, you should use `let rec`. You can also perform pattern matching using `let`. For example, assume `p` has type `Nat × Nat`, then you can write ``` let (x, y) := p x + y ``` let expression that is in tail position.

In particular, the discriminant of a Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expression, the condition of an termIfThenElse : term`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to return `t` or `e` depending on whether `c` is true or false. The explicit argument `c : Prop` does not have any actual computational content, but there is an additional `[Decidable c]` argument synthesized by typeclass inference which actually determines how to evaluate `c` to true or false. Write `if h : c then t else e` instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact that `c` is true/false. if expression and the arguments of functions are not tail positions.

Loops are Tail Recursive Functions

Because the function body itself is a tail position, the infinitely looping function loop is tail recursive. It can be defined as a partial fixpoint.

def loop (x : Nat) : Nat := loop (x + 1) partial_fixpoint
Tail Recursion with Branching

Array.find could also be constructed using well-founded recursion with a termination proof, but may be more convenient to define using Lean.Parser.Command.declaration : commandpartial_fixpoint, where no termination proof is needed.

def Array.find (xs : Array α) (p : α Bool) (i : Nat := 0) : Option α := if h : i < xs.size then if p xs[i] then some xs[i] else Array.find xs p (i + 1) else none partial_fixpoint

If the result of the recursive call is not just returned, but passed to another function, it is not in tail position and this definition fails.

def List.findIndex (xs : List α) (p : α Bool) : Int := match xs with | [] => -1 | x::ys => if p x then 0 else have r := Could not prove 'List.findIndex' to be monotone in its recursive calls: Cannot eliminate recursive call `List.findIndex ys p` enclosed in have r := ys✝.findIndex p; if r = -1 then -1 else r + 1 List.findIndex ys p if r = -1 then -1 else r + 1 partial_fixpoint

The error message on the recursive call is:

Could not prove 'List.findIndex' to be monotone in its recursive calls:
  Cannot eliminate recursive call `List.findIndex ys p` enclosed in
    have r := ys✝.findIndex p;
    if r = -1 then -1 else r + 1
  

7.6.4.2. Monadic functions🔗

Defining a function as a partial fixpoint is more powerful if the function's return type is a monad that is an instance of Lean.Order.MonoBind, such as Option. In this case, recursive call are not restricted to tail-positions, but may also occur inside higher-order monadic functions such as bind and List.mapM.

The set of higher-order functions for which this works is extensible, so no exhaustive list is given here. The aspiration is that a monadic recursive function definition that is built using abstract monadic operations like bind, but that does not open the abstraction of the monad (e.g. by matching on the Option value), is accepted. In particular, using Lean.Parser.Term.do : termdo-notation should work.

Monadic functions

The following function implements the Ackermann function in the Option monad, and is accepted without an (explicit or implicit) termination proof:

def ack : (n m : Nat) Option Nat | 0, y => some (y+1) | x+1, 0 => ack x 1 | x+1, y+1 => do ack x ( ack (x+1) y) partial_fixpoint

Recursive calls may also occur within higher-order functions such as List.mapM, if they are set up appropriately, and Lean.Parser.Term.do : termdo-notation:

structure Tree where cs : List Tree def Tree.rev (t : Tree) : Option Tree := do Tree.mk ( t.cs.reverse.mapM (Tree.rev ·)) partial_fixpoint def Tree.rev' (t : Tree) : Option Tree := do let mut cs := [] for c in t.cs do cs := ( c.rev') :: cs return Tree.mk cs partial_fixpoint

Pattern matching on the result of the recursive call will prevent the definition by partial fixpoint from going through:

def List.findIndex (xs : List α) (p : α Bool) : Option Nat := match xs with | [] => none | x::ys => if p x then some 0 else match Could not prove 'List.findIndex' to be monotone in its recursive calls: Cannot eliminate recursive call `List.findIndex ys p` enclosed in match ys✝.findIndex p with | none => none | some r => some (r + 1) List.findIndex ys p with | none => none | some r => some (r + 1) partial_fixpoint
Could not prove 'List.findIndex' to be monotone in its recursive calls:
  Cannot eliminate recursive call `List.findIndex ys p` enclosed in
    match ys✝.findIndex p with
    | none => none
    | some r => some (r + 1)
  

In this particular case, using Functor.map instead of explicit pattern matching helps:

def List.findIndex (xs : List α) (p : α Bool) : Option Nat := match xs with | [] => none | x::ys => if p x then some 0 else (· + 1) <$> List.findIndex ys p partial_fixpoint

7.6.4.3. Partial Correctness Theorems🔗

For every function defined as a partial fixpoint, Lean proves that the defining equation is satisfied. This enables proofs by rewriting. However, these equational theorems are not sufficient for reasoning about the behavior of the function on arguments for which the function specification does not terminate. Code paths that lead to infinite recursion at runtime would end up as infinite chains of rewrites in a potential proof.

Partial fixpoints in suitable monads, on the other hand, provide additional theorems that map the undefined values from non-termination to suitable values in the monad. In the Option monad, then partial fixpoint equals Option.none on all function inputs for which the defining equation specifies non-termination. From this fact, Lean proves a partial correctness theorem for the function which allows facts to be concluded when the function's result is Option.some.

Partial Correctness Theorem

Recall List.findIndex from an earlier example:

def List.findIndex (xs : List α) (p : α Bool) : Option Nat := match xs with | [] => none | x::ys => if p x then some 0 else (· + 1) <$> List.findIndex ys p partial_fixpoint

With this function definition, Lean automatically proves the following partial correctness theorem:

List.findIndex.partial_correctness.{u_1} {α : Type u_1} (p : α Bool) (motive : List α Nat Prop) (h : (findIndex : List α Option Nat), ( (xs : List α) (r : Nat), findIndex xs = some r motive xs r) (xs : List α) (r : Nat), (match xs with | [] => none | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r motive xs r) (xs : List α) (r : Nat) : xs.findIndex p = some r motive xs r

Here, the motive is a relation between the parameter and return types of List.findIndex, with the Option removed from the return type. If, when given an arbitrary partial function with a signature that's compatible with List.findIndex, the following hold:

  • the motive is satisfied for all inputs for which the arbitrary function returns a value (rather than none),

  • taking one rewriting step with the defining equation, in which the recursive calls are replaced by the arbitrary function, also implies the satisfaction of the motive

then the motive is satsified for all inputs for which the List.findIndex returns some.

The partial correctness theorem is a reasoning principle. It can be used to prove that the resulting number is a valid index in the list and that the predicate holds for that index:

theorem List.findIndex_implies_pred (xs : List α) (p : α Bool) : xs.findIndex p = some i x, xs[i]? = some x p x := α:Type u_1i:Natxs:List αp:α Boolxs.findIndex p = some i x, xs[i]? = some x p x = true α:Type u_1i:Natxs:List αp:α Bool (findIndex : List α Option Nat), (∀ (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = true) (xs : List α) (r : Nat), (match xs with | [] => none | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r x, xs[r]? = some x p x = true α:Type u_1i:Natxs✝:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truexs:List αr:Nathsome:(match xs with | [] => none | x :: ys => if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r x, xs[r]? = some x p x = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αhsome:none = some r x, [][r]? = some x p x = trueα:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx✝:αys✝:List αhsome:(if p x✝ = true then some 0 else (fun x => x + 1) <$> findIndex ys✝) = some r x, (x✝ :: ys✝)[r]? = some x p x = true next α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αhsome:none = some r x, [][r]? = some x p x = true All goals completed! 🐙 next x ys α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αhsome:(if p x = true then some 0 else (fun x => x + 1) <$> findIndex ys) = some r x_1, (x :: ys)[r]? = some x_1 p x_1 = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some r x_1, (x :: ys)[r]? = some x_1 p x_1 = trueα:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:(fun x => x + 1) <$> findIndex ys = some r x_1, (x :: ys)[r]? = some x_1 p x_1 = true next α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:p x = truehsome:some 0 = some r x_1, (x :: ys)[r]? = some x_1 p x_1 = true have : r = 0 := α:Type u_1i:Natxs:List αp:α Boolxs.findIndex p = some i x, xs[i]? = some x p x = true All goals completed! 🐙 All goals completed! 🐙 next α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome:(fun x => x + 1) <$> findIndex ys = some r x_1, (x :: ys)[r]? = some x_1 p x_1 = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truer:Natxs✝:List αx:αys:List αh✝:¬p x = truehsome: a, findIndex ys = some a a + 1 = r x_1, (x :: ys)[r]? = some x_1 p x_1 = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natih: (xs : List α) (r : Nat), findIndex xs = some r x, xs[r]? = some x p x = truexs✝:List αx:αys:List αh✝:¬p x = truer':Nathr:findIndex ys = some r' x_1, (x :: ys)[r' + 1]? = some x_1 p x_1 = true α:Type u_1i:Natxs:List αp:α BoolfindIndex:List α Option Natxs✝:List αx:αys:List αh✝:¬p x = truer':Nathr:findIndex ys = some r'ih: x, ys[r']? = some x p x = true x_1, (x :: ys)[r' + 1]? = some x_1 p x_1 = true All goals completed! 🐙

7.6.4.4. Mutual Recursion with Partial Fixpoints🔗

Lean supports the definition of mutually recursive functions using partial fixpoint. Mutual recursion may be introduced using a mutual block, but it also results from Lean.Parser.Term.letrec : termlet rec expressions and Lean.Parser.Command.declaration : commandwhere blocks. The rules for mutual well-founded recursion are applied to a group of actually mutually recursive, lifted definitions, that results from the elaboration steps for mutual groups.

If all functions in the mutual group have the Lean.Parser.Command.declaration : commandpartial_fixpoint clause, then this strategy is used.

7.6.4.5. Theory and Construction🔗

The construction builds on a variant of the Knaster–Tarski theorem: In a chain-complete partial order, every monotone function has a least fixed point.

The necessary theory is found in the Lean.Order namespace. This is not meant to be a general purpose library of order theoretic results. Instead, the definitions and theorems in Lean.Order are only intended as implementation details of the Lean.Parser.Command.declaration : commandpartial_fixpoint feature, and they should be considered a private API that may change without notice.

The notion of a partial order, and that of a chain-complete partial order, are represented by the type classes Lean.Order.PartialOrder and Lean.Order.CCPO, respectively.

🔗type class
Lean.Order.PartialOrder.{u} (α : Sort u) : Sort (max 1 u)
Lean.Order.PartialOrder.{u} (α : Sort u) : Sort (max 1 u)

A partial order is a reflexive, transitive and antisymmetric relation.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

Instance Constructor

Lean.Order.PartialOrder.mk.{u}

Methods

rel : α  α  Prop

A “less-or-equal-to” or “approximates” relation.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

rel_refl :  {x : α}, x  x

The “less-or-equal-to” or “approximates” relation is reflexive.

rel_trans :  {x y z : α}, x  y  y  z  x  z

The “less-or-equal-to” or “approximates” relation is transitive.

rel_antisymm :  {x y : α}, x  y  y  x  x = y

The “less-or-equal-to” or “approximates” relation is antisymmetric.

🔗type class
Lean.Order.CCPO.{u} (α : Sort u) : Sort (max 1 u)
Lean.Order.CCPO.{u} (α : Sort u) : Sort (max 1 u)

A chain-complete partial order (CCPO) is a partial order where every chain has a least upper bound.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

Instance Constructor

Lean.Order.CCPO.mk.{u}

Extends

Methods

rel : α  α  Prop
Inherited from
  1. PartialOrder α
rel_refl :  {x : α}, x  x
Inherited from
  1. PartialOrder α
rel_trans :  {x y z : α}, x  y  y  z  x  z
Inherited from
  1. PartialOrder α
rel_antisymm :  {x y : α}, x  y  y  x  x = y
Inherited from
  1. PartialOrder α
csup : (α  Prop)  α

The least upper bound of a chain.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

csup_spec :  {x : α} {c : α  Prop}, chain c  (CCPO.csup c  x   (y : α), c y  y  x)

csup c is the least upper bound of the chain c when all elements x that are at least as large as csup c are at least as large as all elements of c, and vice versa.

A function is monotone if it preserves partial orders. That is, if x y then f x f y. The operator represent Lean.Order.PartialOrder.rel.

🔗def
Lean.Order.monotone.{u, v} {α : Sort u} [PartialOrder α] {β : Sort v} [PartialOrder β] (f : α β) : Prop
Lean.Order.monotone.{u, v} {α : Sort u} [PartialOrder α] {β : Sort v} [PartialOrder β] (f : α β) : Prop

A function is monotone if it maps related elements to related elements.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

The fixpoint of a monotone function can be taken using fix, which indeed constructs a fixpoint, as shown by fix_eq,

🔗def
Lean.Order.fix.{u} {α : Sort u} [CCPO α] (f : α α) (hmono : monotone f) : α
Lean.Order.fix.{u} {α : Sort u} [CCPO α] (f : α α) (hmono : monotone f) : α

The least fixpoint of a monotone function is the least upper bound of its transfinite iteration.

The monotone f assumption is not strictly necessarily for the definition, but without this the definition is not very meaningful and it simplifies applying theorems like fix_eq if every use of fix already has the monotonicty requirement.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

🔗theorem
Lean.Order.fix_eq.{u} {α : Sort u} [CCPO α] {f : α α} (hf : monotone f) : fix f hf = f (fix f hf)
Lean.Order.fix_eq.{u} {α : Sort u} [CCPO α] {f : α α} (hf : monotone f) : fix f hf = f (fix f hf)

The main fixpoint theorem for fixedpoints of monotone functions in chain-complete partial orders.

This is intended to be used in the construction of partial_fixpoint, and not meant to be used otherwise.

To construct the partial fixpoint, Lean first synthesizes a suitable CCPO instance.

  • If the function's result type has a dedicated instance, like Option has with instCCPOOption, this is used together with the instance for the function type, instCCPOPi, to construct an instance for the whole function's type.

  • Otherwise, if the function's type can be shown to be inhabited by a witness w, then the instance FlatOrder.instCCPO for the wrapper type FlatOrder w is used. In this order, w is a least element and all other elements are incomparable.

Next, the recursive calls in the right-hand side of the function definitions are abstracted; this turns into the argument f of fix. The monotonicity requirement is solved by the monotonicity tactic, which applies compositional monotonicity lemmas in a syntax-driven way.

The tactic solves goals of the form monotone (fun x => x ) using the following steps:

  • Applying monotone_const when there is no dependency on x left.

  • Splitting on Lean.Parser.Term.match : termPattern matching. `match e, ... with | p, ... => f | ...` matches each given term `e` against each pattern `p` of a match alternative. When all patterns of an alternative match, the `match` term evaluates to the value of the corresponding right-hand side `f` with the pattern variables bound to the respective matched values. If used as `match h : e, ... with | p, ... => f | ...`, `h : e = p` is available within `f`. When not constructing a proof, `match` does not automatically substitute variables matched on in dependent variables' types. Use `match (generalizing := true) ...` to enforce this. Syntax quotations can also be used in a pattern match. This matches a `Syntax` value against quotations, pattern variables, or `_`. Quoted identifiers only match identical identifiers - custom matching such as by the preresolved names only should be done explicitly. `Syntax.atom`s are ignored during matching by default except when part of a built-in literal. For users introducing new atoms, we recommend wrapping them in dedicated syntax kinds if they should participate in matching. For example, in ```lean syntax "c" ("foo" <|> "bar") ... ``` `foo` and `bar` are indistinguishable during matching, but in ```lean syntax foo := "foo" syntax "c" (foo <|> "bar") ... ``` they are not. match expressions.

  • Splitting on termIfThenElse : term`if c then t else e` is notation for `ite c t e`, "if-then-else", which decides to return `t` or `e` depending on whether `c` is true or false. The explicit argument `c : Prop` does not have any actual computational content, but there is an additional `[Decidable c]` argument synthesized by typeclass inference which actually determines how to evaluate `c` to true or false. Write `if h : c then t else e` instead for a "dependent if-then-else" `dite`, which allows `t`/`e` to use the fact that `c` is true/false. if expressions.

  • Moving Lean.Parser.Term.let : term`let` is used to declare a local definition. Example: ``` let x := 1 let y := x + 1 x + y ``` Since functions are first class citizens in Lean, you can use `let` to declare local functions too. ``` let double := fun x => 2*x double (double 3) ``` For recursive definitions, you should use `let rec`. You can also perform pattern matching using `let`. For example, assume `p` has type `Nat × Nat`, then you can write ``` let (x, y) := p x + y ``` let expression to the context, if the value and type do not depend on x.

  • Zeta-reducing a Lean.Parser.Term.let : term`let` is used to declare a local definition. Example: ``` let x := 1 let y := x + 1 x + y ``` Since functions are first class citizens in Lean, you can use `let` to declare local functions too. ``` let double := fun x => 2*x double (double 3) ``` For recursive definitions, you should use `let rec`. You can also perform pattern matching using `let`. For example, assume `p` has type `Nat × Nat`, then you can write ``` let (x, y) := p x + y ``` let expression when value and type do depend on x.

  • Applying lemmas annotated with partial_fixpoint_monotone

The following monotonicity lemmas are registered, and should allow recursive calls under the given higher-order functions in the arguments indicated by · (but not the other arguments, shown as _).

Theorem

Pattern

monotone_allM

Array.allM · _ _ _

monotone_anyM

Array.anyM · _ _ _

monotone_anyM_loop

Array.anyM.loop · _ _ _

monotone_array_filterMapM

Array.filterMapM · _

monotone_array_forM

Array.forM · _ _ _

monotone_array_forRevM

Array.forRevM · _ _ _

monotone_findIdxM?

Array.findIdxM? · _

monotone_findM?

Array.findM? · _

monotone_findRevM?

Array.findRevM? · _

monotone_findSomeM?

Array.findSomeM? · _

monotone_findSomeRevM?

Array.findSomeRevM? · _

monotone_flatMapM

Array.flatMapM · _

monotone_foldlM

Array.foldlM · _ _ _ _

monotone_foldlM_loop

Array.foldlM.loop · _ _ _ _ _

monotone_foldrM

Array.foldrM · _ _ _ _

monotone_foldrM_fold

Array.foldrM.fold · _ _ _ _

monotone_forIn

forIn _ _ ·

monotone_forIn'

forIn' _ _ ·

monotone_forIn'_loop

Array.forIn'.loop _ · _ _

monotone_mapFinIdxM

_.mapFinIdxM ·

monotone_mapM

Array.mapM · _

monotone_modifyM

_.modifyM _ ·

monotone_map

_ <$> ·

monotone_allM

List.allM · _

monotone_anyM

List.anyM · _

monotone_filterAuxM

List.filterAuxM · _ _

monotone_filterM

List.filterM · _

monotone_filterRevM

List.filterRevM · _

monotone_findM?

List.findM? · _

monotone_findSomeM?

List.findSomeM? · _

monotone_foldlM

List.foldlM · _ _

monotone_foldrM

List.foldrM · _ _

monotone_forIn

forIn _ _ ·

monotone_forIn'

forIn' _ _ ·

monotone_forIn'_loop

List.forIn'.loop _ · _ _

monotone_forM

_.forM ·

monotone_mapM

List.mapM · _

monotone_bindM

Option.bindM · _

monotone_elimM

Option.elimM · · ·

monotone_getDM

_.getDM ·

monotone_mapM

Option.mapM · _

monotone_fst

·.fst

monotone_mk

·, ·

monotone_snd

·.snd

monotone_seq

· <*> ·

monotone_seqLeft

· <* ·

monotone_seqRight

· *> ·

coind_monotone_and

· ·

coind_monotone_exists

Exists ·

coind_monotone_forall

(y : _), _ _ _

coind_monotone_or

· ·

implication_order_monotone_and

· ·

implication_order_monotone_exists

Exists ·

implication_order_monotone_forall

(y : _), _ _ _

implication_order_monotone_or

· ·

monotone_bind

· >>= ·

monotone_dite

dite _ · ·

monotone_ite

if _ then · else ·

7.6.5. Partial and Unsafe Definitions🔗

大多数 Lean 函数既可在 Lean 的类型论中进行推理,也可被编译并运行;但凡被标记为 partialunsafe 的定义,则无法在逻辑层面进行有意义的推理。 从逻辑视角看,partial 函数是不透明常量;而凡是引用 unsafe 定义的定理都会被直接拒绝。 作为无法用于推理的交换条件,这些定义受到的约束大幅减少:这使得一些原本不切实际或成本过高而难以给出证明的程序仍然可以编写,同时又不牺牲其余部分的形式化推理。 本质上,Lean 的 partial 子集是一种传统的函数式编程语言,但与定理证明功能深度集成;而 unsafe 子集则在少数情形下允许打破 Lean 的运行时不变式,但相应地与定理证明功能的集成程度较低。 类似地,noncomputable 定义可以使用在程序中不合语义、但在逻辑中有意义的特性。

7.6.5.1. Partial Functions🔗

partial 修饰符只能用于函数定义。 偏函数无需展示终止性,Lean 也不会尝试证明它终止。 之所以称为“偏”,是因为它们未必为定义域中的每个元素指定到余域元素的映射:对某些(乃至所有)输入,它们可能无法终止。 这类定义会被繁释为包含显式递归的 预定义 并由内核进行类型检查;不过在逻辑层面它们随后会被当作不透明常量。

函数的返回类型必须是可被占据(inhabited)的;这可确保自洽性。 否则,偏函数就可能拥有诸如 Unit Empty 的类型。 结合 Empty.elim,即便该函数并不归约,也可以据此“证明” False

对于偏定义,内核负责以下检查:

  • 确认预定义的类型确为一个良构类型;

  • 确认预定义的类型是函数类型;

  • 通过需求 NonemptyInhabited 实例,确保函数的余域是可被占据的;

  • 在“假设 Lean 拥有递归定义”的前提下,检查生成项会通过类型检查。

尽管递归定义不是内核类型论的一部分,仍然可以用内核来检查定义体是否具有正确的类型。 其工作方式与其他函数式语言相同:在一个“该定义已与其类型绑定”的环境中检查定义体,从而为递归的使用做类型检查。 一旦确认通过类型检查,定义体会被丢弃,内核仅保留那个不透明常量。 与所有 Lean 函数一样,编译器会基于繁释得到的 预定义 生成代码。

即便内核不会对偏函数展开,仍可以在不依赖其具体实现的前提下,对调用它们的其他函数开展推理。

证明中的偏函数

递归函数 nextPrime 通过对候选数做试除测试来计算给定数之后的下一个素数,这样的做法效率不高。 由于素数是无限多的,它总是会终止;然而要正式给出这一点的证明并不容易,因此它被标记为 partial

def isPrime (n : Nat) : Bool := Id.run do for i in [2:n] do if i * i > n then return true if n % i = 0 then return false return true partial def nextPrime (n : Nat) : Nat := let n := n + 1 if isPrime n then n else nextPrime n

尽管如此,仍然可以证明下面两个函数是相等的:

def answerUser (n : Nat) : String := s!"The next prime is {nextPrime n}" def answerOtherUser (n : Nat) : String := " ".intercalate [ "The", "next", "prime", "is", toString (nextPrime n) ]

证明包含两步 simp,用来展示这两个函数在语法上并不相同。 尤其是,字符串插值的反糖导致 answerUser 的结果末尾多了一个 toString ""

theorem answer_eq_other : answerUser = answerOtherUser := answerUser = answerOtherUser n:NatanswerUser n = answerOtherUser n n:NattoString "The next prime is " ++ toString (nextPrime n) ++ toString "" = " ".intercalate ["The", "next", "prime", "is", toString (nextPrime n)] n:Nat"The next prime is " ++ (nextPrime n).repr = " ".intercalate ["The", "next", "prime", "is", (nextPrime n).repr] All goals completed! 🐙

7.6.5.2. Unsafe Definitions🔗

不安全定义的保障比偏函数更少。 它们的余域不必是可被占据的,且不限于函数定义;同时还能使用一些可能违反内部不变式或破坏抽象的 Lean 特性。 因此,它们完全不能用作数学推理的一部分。

类型论会把偏函数当作不透明常量处理;而不安全定义只能被其他不安全定义引用。 因此,任何调用了不安全函数的函数本身也必须是不安全的;定理则不允许被声明为不安全。

除了不受限制地使用递归之外,不安全函数还能在类型间强制转换、检查两个值是否为内存中的同一对象、读取指针值、以及在原本纯净的代码中运行 IO 动作。 使用这些算子需要对 Lean 的实现有深入理解。

🔗unsafe def
unsafeCast.{u, v} {α : Sort u} {β : Sort v} (a : α) : β
unsafeCast.{u, v} {α : Sort u} {β : Sort v} (a : α) : β

This function will cast a value of type α to type β, and is a no-op in the compiler. This function is extremely dangerous because there is no guarantee that types α and β have the same data representation, and this can lead to memory unsafety. It is also logically unsound, since you could just cast True to False. For all those reasons this function is marked as unsafe.

It is implemented by lifting both α and β into a common universe, and then using cast (lcProof : ULift (PLift α) = ULift (PLift β)) to actually perform the cast. All these operations are no-ops in the compiler.

Using this function correctly requires some knowledge of the data representation of the source and target types. Some general classes of casts which are safe in the current runtime:

  • Array α to Array β where α and β have compatible representations, or more generally for other inductive types.

  • Quot α r and α.

  • @Subtype α p and α, or generally any structure containing only one non-Prop field of type α.

  • Casting α to/from NonScalar when α is a boxed generic type (i.e. a function that accepts an arbitrary type α and is not specialized to a scalar type like UInt8).

🔗unsafe def
ptrEq.{u_1} {α : Type u_1} (a b : α) : Bool
ptrEq.{u_1} {α : Type u_1} (a b : α) : Bool

Compares two objects for pointer equality.

Two objects are pointer-equal if, at runtime, they are allocated at exactly the same address. This function is unsafe because it can distinguish between definitionally equal values.

🔗unsafe def
ptrEqList.{u_1} {α : Type u_1} (as bs : List α) : Bool
ptrEqList.{u_1} {α : Type u_1} (as bs : List α) : Bool

Compares two lists of objects for element-wise pointer equality. Returns true if both lists are the same length and the objects at the corresponding indices of each list are pointer-equal.

Two objects are pointer-equal if, at runtime, they are allocated at exactly the same address. This function is unsafe because it can distinguish between definitionally equal values.

🔗unsafe opaque
ptrAddrUnsafe.{u} {α : Type u} (a : α) : USize
ptrAddrUnsafe.{u} {α : Type u} (a : α) : USize

Returns the address at which an object is allocated.

This function is unsafe because it can distinguish between definitionally equal values.

🔗unsafe opaque
isExclusiveUnsafe.{u} {α : Type u} (a : α) : Bool
isExclusiveUnsafe.{u} {α : Type u} (a : α) : Bool

Returns true if a is an exclusive object.

An object is exclusive if it is single-threaded and its reference counter is 1. This function is unsafe because it can distinguish between definitionally equal values.

🔗unsafe def
unsafeIO {α : Type} (fn : IO α) : Except IO.Error α
unsafeIO {α : Type} (fn : IO α) : Except IO.Error α

A monad that can have side effects on the external world or throw exceptions of type ε.

BaseIO is a version of this monad that cannot throw exceptions. IO sets the exception type to IO.Error.

🔗unsafe def
unsafeEIO {ε α : Type} (fn : EIO ε α) : Except ε α
unsafeEIO {ε α : Type} (fn : EIO ε α) : Except ε α

Executes arbitrary side effects in a pure context, with exceptions indicated via Except. This a dangerous operation that can easily undermine important assumptions about the meaning of Lean programs, and it should only be used with great care and a thorough understanding of compiler internals, and even then only to implement observationally pure operations.

This function is not a good way to convert an EIO α or IO α into an α. Instead, use do-notation.

Because the resulting value is treated as a side-effect-free term, the compiler may re-order, duplicate, or delete calls to this function. The side effect may even be hoisted into a constant, causing the side effect to occur at initialization time, even if it would otherwise never be called.

🔗unsafe def
unsafeBaseIO {α : Type} (fn : BaseIO α) : α
unsafeBaseIO {α : Type} (fn : BaseIO α) : α

Executes arbitrary side effects in a pure context. This a dangerous operation that can easily undermine important assumptions about the meaning of Lean programs, and it should only be used with great care and a thorough understanding of compiler internals, and even then only to implement observationally pure operations.

This function is not a good way to convert a BaseIO α into an α. Instead, use do-notation.

Because the resulting value is treated as a side-effect-free term, the compiler may re-order, duplicate, or delete calls to this function. The side effect may even be hoisted into a constant, causing the side effect to occur at initialization time, even if it would otherwise never be called.

不安全算子经常被用来利用底层细节编写高性能代码。 类似于通过 FFI 在运行时用 C 代码替换 Lean 代码的方式, 也可以在运行时程序中用不安全 Lean 代码替换安全 Lean 代码。 这可以通过在待替换的函数(通常是 opaque 定义)上添加 implemented_by 属性来实现。 这并不会威胁 Lean 作为逻辑的自洽性:被替换的常量已通过内核检查,而不安全替代仅用于运行时代码。 但这仍然是有风险的——无论是 C 代码还是不安全代码,都可能执行任意副作用。

attribute替换运行时实现

implemented_by 属性指示编译器在已编译代码中将某个常量替换为另一个常量。 被替换上去的常量可以是不安全的。

attr ::= ...
    | implemented_by ident
使用指针检查相等性

通常,BEq 实例的相等判定需要完全遍历两个参数以判断它们是否相等。 如果它们其实就是内存中的同一个对象,这样的遍历就显得很浪费。 在遍历之前先做一次指针相等性测试,可以尽早捕获这种情况。

比较的类型是 Tree(二叉树):

inductive Tree α where | empty | branch (left : Tree α) (val : α) (right : Tree α)

一个不安全函数可以用指针相等来更快地结束结构相等性测试;当指针不相等时,再回退到结构检查:

unsafe def Tree.fastBEq [BEq α] (t1 t2 : Tree α) : Bool := if ptrEq t1 t2 then true else match t1, t2 with | .empty, .empty => true | .branch l1 x r1, .branch l2 y r2 => if ptrEq x y || x == y then l1.fastBEq l2 && r1.fastBEq r2 else false | _, _ => false

在一个不透明定义上添加 implemented_by 属性,就能在安全与不安全代码之间搭桥:

@[implemented_by Tree.fastBEq] opaque Tree.beq [BEq α] (t1 t2 : Tree α) : Bool instance [BEq α] : BEq (Tree α) where beq := Tree.beq
利用运行时表示

由于 Fin 与其底层的 Nat 具有相同的运行时表示,List.map Fin.val 可以用 unsafeCast 来替换,从而避免一次在实践中“什么也没做”的线性时间遍历:

unsafe def unFinImpl (xs : List (Fin n)) : List Nat := unsafeCast xs @[implemented_by unFinImpl] def unFin (xs : List (Fin n)) : List Nat := xs.map Fin.val

从 Lean 内核的视角看,unFin 是用 List.map 定义的:

theorem unFin_length_eq_length {xs : List (Fin n)} : (unFin xs).length = xs.length := n:Natxs:List (Fin n)(unFin xs).length = xs.length All goals completed! 🐙

在已编译代码中,则不会发生对该列表的遍历。

这种替换方式具有风险:证明与已编译代码之间的一致性完全依赖于两个实现的等价性,而这点无法在 Lean 中证明。 这种一致性依赖 Lean 实现层面的细节。 这些“逃逸舱门”应当非常谨慎地使用。

7.6.6. Controlling Reduction🔗

在检查证明与程序时,Lean 会考虑 可约性(亦称“透明性”)。 某个定义的可约性决定了它在繁释与证明执行过程中会被展开的上下文。

可约性有三个层级:

可约

可约定义基本在各处按需展开。 类型类实例合成、定义相等性检查,以及语言中的其它机制都会将此类定义视为一种近似“缩写”的存在。 Lean.Parser.Command.declaration : commandabbrev 命令即应用了这一设定。

半可约

半可约定义不会被潜在昂贵的自动化流程(如类型类实例合成或 simp)展开,但在进行定义相等性检查或解析广义字段记法时会展开。 Lean.Parser.Command.declaration : commanddef 命令通常会创建半可约定义,除非通过属性显式指定了不同的可约性;不过,采用良构递归的定义默认是不可约的。

不可约

不可约定义在繁释期间完全不会被展开。 可通过添加 irreducible 属性将某个定义设为不可约。

可约性与实例合成

下面这三个 String 的别名分别是可约、半可约与不可约:

abbrev Phrase := String def Clause := String @[irreducible] def Utterance := String

在繁释器进行定义相等检查时,可约与半可约别名会被展开,从而被视为与 String 等价:

def hello : Phrase := "Hello" def goodMorning : Clause := "Good morning"

相对地,不可约别名不会在定义相等测试中被展开,因此作为字符串的类型会被拒绝:

def goodEvening : Utterance := type mismatch "Good evening" has type String : Type but is expected to have type Utterance : Type"Good evening"
type mismatch
  "Good evening"
has type
  String : Type
but is expected to have type
  Utterance : Type

由于 Phrase 是可约的,ToString String 实例可被当作 ToString Phrase 实例来用:

instToStringString#synth ToString Phrase

然而 Clause 是半可约的,因此不能直接使用 ToString String 实例:

failed to synthesize ToString Clause Additional diagnostic information may be available using the `set_option diagnostics true` command.#synth ToString Clause
failed to synthesize
  ToString Clause

Additional diagnostic information may be available using the `set_option diagnostics true` command.

可以显式启用该实例:构造一个会化简为 ToString String 实例的 ToString Clause 实例。 该示例之所以可行,是因为在进行定义相等检查时会展开半可约定义:

instance : ToString Clause := inferInstanceAs (ToString String)
可约性与广义字段记法

在查找匹配名称时,广义字段记法 会展开可约与半可约的声明。 给定 List 的一个半可约别名 Sequence

def Sequence := List def Sequence.ofList (xs : List α) : Sequence α := xs

广义字段记法允许从类型为 Sequence Nat 的项上访问 List.reverse

let xs := Sequence.ofList [1, 2, 3]; List.reverse xs : List Nat#check let xs : Sequence Nat := .ofList [1,2,3]; xs.reverse

然而,一旦将 Sequence 声明为不可约,就会阻止展开:

attribute [irreducible] Sequence let xs := Sequence.ofList [1, 2, 3]; sorry : ?m.42#check let xs : Sequence Nat := .ofList [1,2,3]; invalid field 'reverse', the environment does not contain 'Sequence.reverse' xs has type Sequence Natxs.reverse
invalid field 'reverse', the environment does not contain 'Sequence.reverse'
  xs
has type
  Sequence Nat
attribute可约性标注

可以使用如下三种可约性属性之一来设置某个定义的可约性:

attr ::= ...
    | reducible
attr ::= ...
    | semireducible
attr ::= ...
    | irreducible

这些属性只能在被修改定义所在的同一文件中全局应用;不过,它们也可以在任意位置以 Lean.Parser.Term.attrKindlocal 方式应用。

7.6.6.1. Reducibility and Tactics

下面这些战术可控制大多数战术会展开哪些定义:with_reduciblewith_reducible_and_instanceswith_unfolding_all

可约性与战术

函数 plussumtally 都是 Nat.add 的同义名,且分别为可约、半可约与不可约:

abbrev plus := Nat.add def sum := Nat.add @[irreducible] def tally := Nat.add

可约同义名会被 simp 展开:

theorem plus_eq_add : plus x y = x + y := x:Naty:Natplus x y = x + y All goals completed! 🐙

半可约同义名则不会被 simp 展开:

theorem sum_eq_add : sum x y = x + y := x:Naty:Natsum x y = x + y simp made no progressx:Naty:Natsum x y = x + y

不过,由 rfl 触发的定义相等检查会展开 sum

theorem sum_eq_add : sum x y = x + y := x:Naty:Natsum x y = x + y All goals completed! 🐙

不可约的 tally 不会被定义相等所化简:

theorem tally_eq_add : tally x y = x + y := x:Naty:Nattally x y = x + y tactic 'rfl' failed, the left-hand side tally x y is not definitionally equal to the right-hand side x + y x y : Nat ⊢ tally x y = x + yx:Naty:Nattally x y = x + y

当显式提供时,simp 可以展开任意定义,甚至包括不可约的:

theorem tally_eq_add : tally x y = x + y := x:Naty:Nattally x y = x + y All goals completed! 🐙

类似地,可将证明的一部分放入 with_unfolding_all 块中以忽略不可约性:

theorem tally_eq_add : tally x y = x + y := x:Naty:Nattally x y = x + y with_unfolding_all All goals completed! 🐙

7.6.6.2. Modifying Reducibility

可以在定义所在的模块中,使用 Lean.Parser.Command.attribute : commandattribute 命令施加相应属性,从而全局修改某个定义的可约性。 在其他模块中,可通过带 local 修饰符的属性应用来修改已导入定义的可约性。 Lean.Parser.commandSeal__ : commandThe `seal foo` command ensures that the definition of `foo` is sealed, meaning it is marked as `[irreducible]`. This command is particularly useful in contexts where you want to prevent the reduction of `foo` in proofs. In terms of functionality, `seal foo` is equivalent to `attribute [local irreducible] foo`. This attribute specifies that `foo` should be treated as irreducible only within the local scope, which helps in maintaining the desired abstraction level without affecting global settings. sealLean.Parser.commandUnseal__ : commandThe `unseal foo` command ensures that the definition of `foo` is unsealed, meaning it is marked as `[semireducible]`, the default reducibility setting. This command is useful when you need to allow some level of reduction of `foo` in proofs. Functionally, `unseal foo` is equivalent to `attribute [local semireducible] foo`. Applying this attribute makes `foo` semireducible only within the local scope. unseal 命令是该流程的便捷写法。

syntax局部不可约性

The seal foo command ensures that the definition of foo is sealed, meaning it is marked as [irreducible]. This command is particularly useful in contexts where you want to prevent the reduction of foo in proofs.

In terms of functionality, seal foo is equivalent to attribute [local irreducible] foo. This attribute specifies that foo should be treated as irreducible only within the local scope, which helps in maintaining the desired abstraction level without affecting global settings.

command ::= ...
    | The `seal foo` command ensures that the definition of `foo` is sealed, meaning it is marked as `[irreducible]`.
This command is particularly useful in contexts where you want to prevent the reduction of `foo` in proofs.

In terms of functionality, `seal foo` is equivalent to `attribute [local irreducible] foo`.
This attribute specifies that `foo` should be treated as irreducible only within the local scope,
which helps in maintaining the desired abstraction level without affecting global settings.
seal ident ident*
syntax局部可约性

The unseal foo command ensures that the definition of foo is unsealed, meaning it is marked as [semireducible], the default reducibility setting. This command is useful when you need to allow some level of reduction of foo in proofs.

Functionally, unseal foo is equivalent to attribute [local semireducible] foo. Applying this attribute makes foo semireducible only within the local scope.

command ::= ...
    | The `unseal foo` command ensures that the definition of `foo` is unsealed, meaning it is marked as `[semireducible]`, the
default reducibility setting. This command is useful when you need to allow some level of reduction of `foo` in proofs.

Functionally, `unseal foo` is equivalent to `attribute [local semireducible] foo`.
Applying this attribute makes `foo` semireducible only within the local scope.
unseal ident ident*

7.6.6.3. Options

出于性能考虑,繁释器与许多战术会构建索引与缓存。 其中不少会考虑可约性;而一旦全局改变了可约性,就无法使这些索引/缓存失效并重新生成。 默认情况下,会禁止对可约性进行可能带来不可预测结果的不安全修改;不过,可通过 allowUnsafeReducibility 选项启用之。

🔗option
allowUnsafeReducibility

Default value: false

enables users to modify the reducibility settings for declarations even when such changes are deemed potentially hazardous. For example, simp and type class resolution maintain term indices where reducible declarations are expanded.